Как должно быть очевидно, одна из целей этого сайта — убедить принимать F# всерьёз в роли универсального языка разработки.

Но в то время как функциональный стиль всё больше проникает в массы, и C# уже получил такие функциональные средства как лямбды и LINQ, кажется, что C# всё больше и больше наступает на пятки F#. Так что, как это ни странно, но я стал всё чаще слышать как высказывают такие мысли:

  • «C# уже обладает большей частью инструментария F#, и зачем мне напрягаться с переходом?»
  • «Нет никакой необходимости что-то менять. Всё, что нам нужно сделать, так это пару лет подождать, и C# получит достаточно от F#, что обеспечит практически все плюшки.»
  • «F# только чуть лучше, чем C#, но не настолько, чтобы в самом деле тратить время с переходом на него.»
  • «F# кажется действительно неплох, хоть и пугает местами. Но я не могу найти ему практического применения, чтобы использовать вместо C#.»

Не сомневаюсь, что теперь, когда и в Java тоже добавлены лямбды, подобные комментарии зазвучали в экосистеме JVM при обсуждении «Scala и Closure против Java».

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

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

Но C# не идеален. Как и в большинстве основных объектно-ориентированных языков, в нём присутствуют проектные решения, которые не могут быть компенсированы никакими прелестями LINQ или лямбд. В этой статье я покажу вам некоторые проблемы, которые вызваны такими решениями, и предложу способы улучшения языка, которые помогут избежать этих проблем.

(Теперь я надеваю свой огнеупорный костюм. Думаю, он мне понадобится!)

ДОПОЛНЕНИЕ: похоже, что многие читатели совсем не поняли эту статью. Позвольте я проясню:

  • я не говорю, что языки со статической типизацией «лучше», чем с динамической.
  • я не говорю, что функциональные языки «лучше», чем объектно-ориентированные.
  • я не говорю, что возможность продумывать код является наиболее важным аспектом языка.

Я говорю, что:

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

Больше ничего. Спасибо!


Всё-таки, что же такое «обоснованный» язык программирования?


Если вы знакомы с функциональными программистами, то часто слышите словосочетание «продумывать что-то», например «мы хотим продумать наши программы».

Что это значит? Зачем использовать слово «продумывать» вместо обычного «понять»? Использование «продумывания» берёт начало в математике и логике, но я собираюсь использовать простое и практичное определение:

  • «продумывание кода» означает что вы можете делать умозаключения, используя только то, что вы видите здесь и сейчас, без раскопок в других блоках программного кода.

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

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

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

Пример 1


Давайте приступим к рассмотрению следующего кода.

  • Мы начинаем с переменной x, которой присваивается целочисленное значение 2.
  • После этого вызывается DoSomething, которой x передаётся как параметр.
  • И после этого переменной y присваивается результат выражения x — 1.

Вопрос, который я задам, не сложен: каково значение y?

var x = 2;
DoSomething(x);

// Каково значение y? 
var y = x - 1;

Ответ
Ответом будет -1. Вы получили такой результат? Нет? Если вы не можете понять это, смотрите далее.

Расшифровка
Шуточный вопрос! Этот код написан на JavaScript! Вот как он выглядит полностью:

function DoSomething (foo) { x = false}

var x = 2;
DoSomething(x);
var y = x - 1;

Да, это ужасно! DoSomething получает доступ к x напрямую, а не через параметр, и (вот тебе раз!) превращает его в переменную с булевым типом. После этого вычитание единицы превращает x из false в 0 (с преобразованием типа), и y в результате получается -1.
Вы полностью взбешены? Простите, что сбил с толку вас подменой языка, но я хотел просто продемонстрировать как это бесит, когда язык ведёт себя непредсказуемым образом.

JavaScript — очень удобный и важный язык. Но никто не может утверждать, что логичность является его сильной стороной. В действительности, в большинстве языков с динамической типизацией присутствуют странности, которые создают трудности для продумывания кода.

Благодаря статической типизации и разумным правилам определения области видимости ошибки такого рода не могут возникать в коде на C# (только, конечно, если не очень постараетесь). В C# если правильно не подобрать типы, возникает ошибка времени компиляции, а не времени выполнения.

Другими словами, C# намного более предсказуем, чем JavaScript. Первый балл за статическую типизацию! Итак мы получили первую рекомендацию для создания предсказуемого языка.

Как сделать язык предсказуемым:

1. Переменным не разрешается менять тип.

C# выглядит неплохо по сравнению с JavaScript. Но мы ещё не закончили…

ДОПОЛНЕНИЕ: Это был заведомо глупый пример. Если бы мог вернуться в прошлое, я бы выбрал пример получше. Да, я знаю, что ни один разумный человек не будет делать так. Но суть дела не меняется: JavaScript не запрещает вам делать глупости с неявным приведением типов.


Пример 2


В следующем примере мы собираемся создать два экземпляра класса Customer с одинаковыми данными в них.
Вопрос: они равны?

 // создаём двух заказчиков
var cust1 = new Customer(99, "J Smith");
var cust2 = new Customer(99, "J Smith");

// истина или ложь?
cust1.Equals(cust2);


Ответ
Кто знает. Это зависит от того, как Customer реализован. Этот код не предсказуем.
Вы должны, по крайней мере, узнать реализован ли в классе IEquatable, и вероятно вы будете должны посмотреть код реализации класса, чтобы понять что конкретно там происходит.

Но почему этот вопрос вообще возникает? Позвольте мне у вас спросить:

— Как часто вы НЕ хотите, чтобы экземпляры были приравнены?
— Как часто вам приходилось переписывать метод Equals?
— Как часто вы сталкивались с ошибкой из-за того, что забыли переопределить метод Equals?
— Как часто вы сталкивались с ошибкой, вызванной неправильной реализацией метода GetHashCode (например, забыли изменить его результат при изменении полей, по которым проводится сравнение)?

Почему не определить по умолчанию сравнение объектов (экземпляров классов) по равенству их атрибутов, а сравнение по равенству их ссылок сделать особым случаем? Так что давайте добавим ещё один пункт в наш список.

Как сделать язык предсказуемым:

1. Переменным не разрешается менять тип.

2. Объекты с идентичным содержанием должны быть равными по умолчанию.

Пример 3


В следующем примере у меня два объекта содержат идентичные данные, но являются экземплярами разных классов. Вопрос тот же: они равны?

// создаём заказчика и счёт
var cust = new Customer(99, "J Smith");
var order = new Order(99, "J Smith");

// истина или ложь?
cust.Equals(order);

Ответ
Ну кого это может волновать! Это определённо ошибка! Во-первых, зачем вообще сравнивать объекты двух различных классов? Сравнивайте их имена или идентификаторы, только не сами объекты. Здесь должна возникать ошибка при компиляции. Но если это не так, то почему бы и нет? Вероятно вы только что ошибочно использовали имя не той переменной, и теперь получили в своём коде ошибку, которую нелегко отловить. Почему ваш язык позволил вам это?

Добавим ещё один пункт в наш лист.

Как сделать язык предсказуемым:

1. Переменным не разрешается менять тип.
2. Объекты с идентичным содержанием должны быть равными по умолчанию.
3. Сравнение объектов с разными типами должно вызывать ошибку времени компиляции.

ДОПОЛНЕНИЕ: многие утверждают, что это необходимо для сравнения классов, находящихся в отношении наследования. Конечно, это верно. Но какова цена этой возможности? Вы получаете возможность сравнивать дочерние классы, но теряете способность обнаруживать случайные ошибки. Что в реальной работе более важно? Это решать вам, я только хотел явно показать что у принятой практики есть и недостатки, а не только преимущества.

Пример 4


В этом фрагменте кода мы просто создадим экземпляр класса Customer. И всё. Не могу придумать что-то более простое.

// создаём заказчика
var cust = new Customer();

// что ожидается на выходе?
Console.WriteLine(cust.Address.Country);

Теперь вопрос в следующем: какой вывод мы ожидаем от WriteLine?

Ответ
Да кто ж его знает. Это зависит будет ли свойство Address равно null или нет. И это что-то, что опять невозможно определить без того, чтобы заглянуть во внутренности класса Customer.

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

Ещё один пункт в наш список.

Как сделать язык предсказуемым:

1. Переменным не разрешается менять тип.
2. Объекты с идентичным содержанием должны быть равными по умолчанию.
3. Сравнение объектов с разными типами должно вызывать ошибку времени компиляции.
4. Объекты всегда должны быть инициализированными до корректного состояния. Невыполнение этого требования должно приводить к ошибке времени компиляции.

Пример 5


В следующем примере мы проделаем следующее:
— создадим заказчика
— добавим его в хеш-множество
— сделаем что-нибудь с объектом заказчика
— попробуем найти заказчика в множестве

Что может пойти не так?

// создаём заказчика
var cust = new Customer(99, "J Smith");

// добавляем его в множество
var processedCustomers = new HashSet<Customer>();
processedCustomers.Add(cust);

// обрабатываем его
ProcessCustomer(cust);

// Он всё ещё в множестве? истина или ложь?
processedCustomers.Contains(cust);

Итак, содержит ли множество объект заказчика после выполнения этого кода?

Ответ
Может быть да. А может и нет.
Это зависит от двух моментов:
— во-первых, зависит ли хеш-код от модифицируемого поля, например, такого как идентификатор?
— во-вторых, меняет ли ProcessCustomer это поле?

Если на оба вопроса ответ утвердительный, хеш будет изменён, и объект заказчика не будет больше виден в этом множестве (хотя он и присутствует где-то там внутри!). Это может привести к падению производительности и проблемам с памятью (например, если множество используется для реализации кеша). Как язык может предотвратить это?

Один способ — сделать немодифицируемыми поля, используемые в GetHashCode, и оставить модифицируемыми все остальные. Но это очень неудобно.

Вместо этого лучше сделать немодифицируемым весь класс Customer! Теперь, если объект Customer неизменяемый, и ProcessCustomer захочет сделать изменения, он будет обязан вернуть новую версию объекта заказчика, а код будет выглядеть например так:

// создаём заказчика
var cust = new Customer(99, "J Smith");

// добавляем его в множество
var processedCustomers = new HashSet<Customer>();
processedCustomers.Add(cust);

// обрабатываем его и возвращаем изменения
var changedCustomer = ProcessCustomer(cust);

// истина или ложь?
processedCustomers.Contains(cust);

Заметьте, что вызов ProcessCustomer изменён на

var changedCustomer = ProcessCustomer(cust);

Теперь ясно, что ProcessCustomer что-то изменяет, если просто посмотреть на эту строку. Если ProcessCustomer ничего не изменяет, то объект и вообще незачем возвращать.

Возвращаясь к обсуждаемому примеру, теперь мы понимаем, что исходная версия объекта заказчика гарантированно будет присутствовать в множестве, что бы ProcessCustomer не пытался проделывать.

Разумеется, это не даёт ответа на вопрос должен ли в множестве присутствовать новый объект, или старый объект (или оба вместе). Но в сравнении с версией с модифицируемым объектом заказчика, описанная проблема теперь упирается вам прямо в лоб и не может быть случайно упущена. Так что немодифицируемость на коне!

Это ещё один пункт в нашем списке.

Как сделать язык предсказуемым:

1. Переменным не разрешается менять тип.
2. Объекты с идентичным содержанием должны быть равными по умолчанию.
3. Сравнение объектов с разными типами должно вызывать ошибку времени компиляции.
4. Объекты должны всегда быть инициализированными в корректное состояние. Невыполнение этого требования должно приводить к ошибке времени компиляции.
5. После создания объекты и коллекции должны оставаться неизменными.

Момент как раз для шутки о немодифицируемости:

— Сколько программистов на Хаскелл нужно, чтобы поменять лампочку?
— Программисты на Хаскелл не меняют лампочки, они их заменяют. И вы должны одновременно заменить весь дом.

Почти закончили — остался один!

Пример 6


В этом заключительном примере мы попробуем получить заказчика из CustomerRepository.

// создаём репозиторий
var repo = new CustomerRepository();

// ищем заказчика по идентификатору
var customer = repo.GetById(42);

// что мы ожидаем на выходе?
Console.WriteLine(customer.Id);

Вопрос: после того как мы выполнили:

var customer = repo.GetById(42)

, каково значение customer.Id?

Ответ
Конечно, это всё неопределённо.

Если я посмотрю на сигнатуру метода GetById, она скажет мне, что он всегда возвращает объект Customer. Но так ли это?

Что происходит, когда заказчик не найден? repo.GetById вернёт null? Или выбросит исключение? Вы не сможете это определить, просто глядя на этот фрагмент.

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

А теперь представьте, что ваш язык не разрешает использовать null и не позволяет выбрасывать исключения. Что бы вы тогда могли сделать?
Чтобы ответить на это, вам пришлось бы возвращать специальный класс, который способен содержать в себе или объект заказчика, или ошибку, например, так:

// создаём репозиторий
var repo = new CustomerRepository();

// ищем заказчика по идентификатору
// и возвращаем результатом объект типа CustomerOrError
var customerOrError = repo.GetById(42);

Код, который будет обрабатывать результат customerOrError, должен проверить какого вида ответ и обработать по отдельности каждый вариант:

// обработать оба случая
if (customerOrError.IsCustomer)
    Console.WriteLine(customerOrError.Customer.Id);

if (customerOrError.IsError)
    Console.WriteLine(customerOrError.ErrorMessage);

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

И теперь, наконец, два последних пункта в списке.

Как сделать язык предсказуемым:

1. Переменным не разрешается менять тип.
2. Объекты с идентичным содержанием должны быть равными по умолчанию.
3. Сравнение объектов с разными типами должно вызывать ошибку времени компиляции.
4. Объекты должны всегда быть инициализированными в корректное состояние. Невыполнение этого требования должно приводить к ошибке времени компиляции.
5. После создания объекты и коллекции должны оставаться неизменными.
6. Запретить использование null.
7. Отсутствие данных или возможность ошибки должны быть представлены явно в сигнатуре метода.


Я мог быть продолжать с примерами, демонстрирующими неправильное использование глобальных переменных, побочные эффекты, приведение типов и т.д. Но думаю, что стоит здесь остановиться — вероятно вы уже догадались!

Может ваш язык программирования делать это?


Надеюсь, очевидно, что такие дополнения помогают сделать язык более обдумываемым. К несчастью, основные ОО языки типа C#, скорее всего, не получат этих приобретений.

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

Кроме того, с точки зрения ОО, способ, которым должны сравниваться два объекта, зависит полностью от их реализации — ОО полностью посвящено полиморфизму поведения, так что компилятор здесь не должен лезть не в своё дело! Это же касается и порядка создания, и порядка инициализации объектов, которые полностью определяются в них самих. Здесь не существует законов, которые говорят разрешать это или не разрешать.

В конечном итоге, очень сложно добавить ссылочные типы с запретом нулевых ссылок в статически типизируемый ОО язык без реализации ограничений инициализации по четвёртому пункту. Как сказал Эрик Липперт "Запрет null-ссылок — это штука, которую легко предусмотреть в системе типов в день первый, но не то, что вы захотите доработать спустя двенадцать лет."

Напротив, в большинстве функциональных языков эти «предсказательные» инструменты реализованы в ядре языка.

Например, в F# все пункты списка, кроме одного, встроены в язык:
1. Значения не могут менять свой тип. (И это включает даже невозможность неявного приведения целого числа к плавающему).
2. Переменные типа запись с идентичным внутренним наполнением РАВНЫ по умолчанию.
3. Сравнение значений с разными типами ВЫЗЫВАЕТ ошибку времени компиляции.
4. Значения ДОЛЖНЫ быть инициализированы до корректного состояния. Невыполнение этого требования приводит к ошибке времени компиляции.
5. После создания объекты НЕ модифицируемы по умолчанию.
6. Значения null НЕ разрешены, в большинстве случаев.

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

Конечно, при работе с F# у вас всё-таки остаётся множество проблемных моментов. Вы можете получить модифицируемые значения, вы можете создавать и бросать исключения, и вы можете на самом деле столкнуться с null, которые передаются из не-F# кода. Но эти проблемы будут возникать в коде как дурно пахнущие кучки, потому что не являются стандартными и не используются в обычной практике.

Другие языки, например, Хаскелл, являются даже более пуританскими, чем F#, а значит даже лучше поддерживают продумывание, но даже на Хаскелле программы не могут быть идеальными. В самом деле, ни один язык не может быть идеально предсказуемым и, в то же время, оставаться практичным. И всё-таки некоторые языки более предсказуемы, чем другие.

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

Лямбды — это не выход


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

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

Комментарии


Дайте-ка я попробую предугадать несколько комментариев…

Комментарий: Эти примеры высосаны из пальца! Если писать аккуратно и следовать правильным методикам, можно писать безопасный код без этих фенечек!

Да, можно. Я не утверждаю, что вы не можете. Но эта статья не о написании безопасного кода, она об обдумывании кода. В этом есть разница.
И она не о том, что вы сможете сделать, если будете аккуратны. Она о том, что может случиться, если вы не будете аккуратны! То есть, помогает ли ваш язык программирования (а не стандарты кодирования, не тесты, не IDE, не методики разработки) размышлять над вашим кодом?

Комментарий: Вы говорите, что язык иметь эти способности обязан. Не очень ли бесцеремонно это с вашей стороны?

Пожалуйста, читайте внимательно. Я вообще не говорю этого. Вот о чём я говорю: ЕСЛИ вы хотите иметь возможность размышлять над кодом, ТОГДА будет значительно легче это делать с языком, который содержит возможности, о которых я упоминал. Если размышление над кодом не имеет значения для вас, пожалуйста, со спокойной душой проигнорируйте всё, что я сказал!

Комментарий: концентрация только на одном аспекте языка программирования слишком ограничивает. Разве другие качества не являются столь же важными?

Да, конечно, являются. Я не деспот в этом вопросе. Я считаю, что такие свойства, как комплексные исчерпывающие библиотеки, хорошие инструменты разработки, дружественное сообщество и жизнеспособность экосистемы также очень важны. Но целью этой статьи было ответить на конкретные вопросы, которые я упоминал в начале, например, «В C# уже присутствует большинство средств F#, зачем беспокоиться и переходить?»

Комментарий: Почему вы так быстро списали динамические языки?
Во-первых, приношу свои извинения разработчикам на JavaScript за шпильку в их адрес! Мне очень нравятся динамические языки, а один из моих любимых языков, Smalltalk, совершенно не обеспечивает размышление над кодом в соответствии с принципами, о которых я говорил. К счастью, эта статья не попытка уговорить вас какие языки считать вообще «лучшими», а всего лишь обсуждение одного из аспектов выбора языка.

Комментарий: немодифицируемые структуры данных медленны, и с ними выполняется громадное количество дополнительных распределений памяти. Не пострадает ли производительность?

Эта статья не пытается оценивать влияние данных свойств языка на производительность (или что-то другое). Но возникает справедливый вопрос — что должно иметь более высокий приоритет: качество кода или производительность? Вам решать, и это зависит от задачи. Лично я в первую очередь смотрю на безопасность и качество, если только нет настоятельного требования не делать так. Вот знак, который мне нравится:



Выводы


Я только что сказал, что эта статья не является попыткой склонить вас к выбору языка с поддержкой «продумывания кода». Но это не совсем правда.
Если вы уже выбрали статически типизированный, высокоуровневый язык (как C# или Java), становится ясно, что «продумывание кода» или что-то наподобие этого было важным критерием при принятии решения.

В таком случае я надеюсь, что примеры в этой статье могут заставить вас подумать об использовании более «рассуждаемого» языка на платформе, которую вы выбрали (.NET или JVM).

Аргумент оставаться со своим языком (- что ваш язык в конечном итоге «догонит») может срабатывать в части свойств языка, но никакое количество будущих улучшений не изменит по-настоящему основную составляющую принципов проектирования в ОО языке. Вы никогда не избавитесь от null, от модифицируемости объектов, или требования каждый раз переопределять методы определения их равенства.

И вот что замечательно в F# или Scala/Clojure — эти альтернативные функциональные языки не заставят вас переходить в другую экосистему, но в то же время мгновенно улучшат качество вашего кода.

Мне кажется, что это сравнительно небольшой риск по отношению к затратам.

Комментарии (195)


  1. kekekeks
    09.10.2015 16:58
    +3

    Есть подозрение, что стоит дождаться, когда запустят таки новую версию Nemerle с поддержкой решарпера и упрощением написания макросов. Ждать осталось не долго, ибо Nitra уже почти доделана.


    1. evilbloodydemon
      09.10.2015 17:53
      +2

      есть подозрение, что ждать еще минимум три года, судя по динамике разработки


      1. kekekeks
        09.10.2015 18:14

        Они меньше чем за год закрыли Milestone 2, а там основное «мясо» нитры. Учитывая, что грамматика немерла у них используется как тестовая и для бутстрапа, думаю, к лету мы увидим что-то работающее. На фоне этого, а так же инопланетной системе именования сущностей в F#, не вижу смысла инвестировать в переезд на оный. Особенно учитывая, что немерл может переваривать сишарпные исходники.


        1. knagaev
          09.10.2015 18:26

          А что значит инопланетная система именования сущностей? Я не в смысле защищаю F#, а правда интересно.


          1. kekekeks
            09.10.2015 19:48

            Несовместимая с основными .NET-языками (C#/VB.NET) и, следовательно, с остальным фреймворком. Потому по отношению к .NET «инопланетная». По факту банальный нижнийКемельКейс где не надо.


            1. knagaev
              09.10.2015 22:20
              +2

              Извините, а пример можно? Просто мне показалось, что там ничего такого инородного нет.


  1. grossws
    09.10.2015 19:21
    +1

    С переводом корнё reason неудобно вышло. Т. к. в части статьи он переводится обосновывать, в части как продумывать… Лишняя путаница, оригинал читается куда легче.


    1. knagaev
      09.10.2015 19:39

      Я хотел перевести так, чтобы донести смысл идей, а там от контекста он сильно меняется.
      Надеюсь, не сильно испортил оригинал :)


      1. grossws
        09.10.2015 19:56

        Надеюсь, что не сильно. Я в этот момент убежал читать оригинал)


  1. palexisru
    09.10.2015 20:19
    -7

    прошу оценить на соответствие требованиям описание учебного языка программирования из http://habrahabr.ru/post/219419/


  1. klirichek
    09.10.2015 20:20
    +6

    К слову, самый первый пример возможен и без всяких javascript.
    Банальная функция на C++, принимающая параметр по ссылке и присваивающая туда 0. И никаких приведений не надо!
    (именно поэтому уважаю гугловский линт, который призывает передавать изменяющиеся параметры по указателю, а не по ссылке)


    1. knagaev
      09.10.2015 22:21

      А для этого тут вообще пункт номер 5.


      1. yatagarasu
        09.10.2015 23:29

        Для пункта 5, в с++ есть Const Correctness.


        1. knagaev
          09.10.2015 23:40

          Так я об этом и говорю :)
          В С++ в соответствии с 5 пунктом тоже не стоит делать модификацию объектов в методе.
          Передавать параметр по ссылке для изменения — верный способ взорвать мозг того, кто будет читать этот код потом.
          Или свой мозг через несколько месяцев.


          1. yatagarasu
            10.10.2015 11:08

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


    1. qw1
      10.10.2015 13:04
      +1

      К слову, самый первый пример возможен и без всяких javascript. Банальная функция на C++

      Вот до такого
      #define var auto

      я бы не додумался


      1. grossws
        10.10.2015 16:54

        А что, хороший метод. Ещё, правда, надо this. заменять ,)


  1. alexeibs
    09.10.2015 21:10
    -8

    Описанные проблемы вызываются банальной невнимательностью. Все это решается линтерами и прочими анализаторами кода, а также юнит-тестами.


    1. lair
      09.10.2015 21:51
      +7

      Угу, вот только статья о том, как читать код глазами.


      1. webkumo
        09.10.2015 21:59
        -4

        Если все соглашения выполняются — то и читать глазками. А если в проекте бардак — то и на «защищённом от дурака» языке можно написать ужас-ужас.


        1. lair
          09.10.2015 21:59
          +7

          Так вот пойнт в том, что приемы, описанные в статье, уменьшают количество способов написать ужас-ужас.


          1. yatagarasu
            09.10.2015 23:24
            -4

            уменьшают количество способов написать ужас-ужас.

            Вот и выросло поколение инженеров, которые не знают закона Мерфи.

            если есть вероятность того, что какая-нибудь неприятность может случиться, то она обязательно произойдёт


            одно из следствий которого кстати

            Если четыре причины возможных неприятностей заранее устранены, то всегда найдётся пятая.


            1. VoidEx
              10.10.2015 00:19
              +2

              > если есть вероятность того, что какая-нибудь неприятность может случиться, то она обязательно произойдёт
              но с одинаковой вероятностью ли?


              1. yatagarasu
                10.10.2015 10:54
                +4

                обязательно произойдёт

                Это по вашему какая вероятность?


                1. VoidEx
                  10.10.2015 14:58

                  По-моему — разная. А по-вашему?


                1. VoidEx
                  10.10.2015 18:16

                  Ну, я поясню. Есть два списка, из 5 и из 1000 элементов, для простоты — чисел от 1 и далее. Список шаффлится рандомно, потом по очереди читается до конца, копируя элементы в вывод. Потом снова. Понятно, что через хотя бы 1000 выведенных элементов мы обязательно встретим там 1 (для любого из списков). Вопрос, с какой вероятностью следующий прочитанный из вывода элемент — 1 для первого и для второго списков? Из какого списка надо читать, чтобы встречать 1 (неприятность) как можно реже?


            1. lair
              10.10.2015 01:27
              +4

              Тем не менее, я предпочту иметь дело с (одной) пятой причиной неприятностей, нежели с четырьмя устраненными.


              1. yatagarasu
                10.10.2015 10:58
                -4

                Ок, по мне так лучше четыре известных, чем одна неизвестная. Вдруг осознать что всё было написано не правильно, за пару дней до дедлайна — бесценное чувство. Особенно, когда ЯП просто ставит тебя перед фактом, что твоя идеально написанная программа не укладывается по паямяти или по быстродействию.


                1. lair
                  10.10.2015 17:01

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

                  Во-вторых, если вы за пару дней до дедлайна узнали, что у вас проблемы с ресурсами, значит, вы не очень хорошо спланировали работу. Если заранее известно, что ресурсы ограничены, тесты на применимость технологии надо начинать очень рано.


                  1. yatagarasu
                    10.10.2015 18:04
                    +1

                    Да, мы уже начали буквально разбирать законы Мерфи, куда мы катимся…

                    Четыре известных могут увеличивать время разработки, только если разработчик упорно игнорирует эти проблемы.
                    Покажите мне статью, в которой разбираются проблемы функциональных языков, проблемы использования иммутабельных данных. Пока я лишь вижу что там «всё замечательно» — а по опыту скажу, что это значит что серьёзно никто эти не занимался, и у каждого есть шанс стать первопроходцем.

                    Парадокс ФЯ конечно в том, что выучив этот подход, можно потом свободно применять его и в с++ и в с# и на java. Но стоит ли переходить на функциональный язык, с его идеологическими ограничениями — большой вопрос.


                    1. lair
                      10.10.2015 19:56

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

                      Что регулярно и происходит.

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

                      Угу, в твиттере скала лет пять уже, эрланг в эриксоне двадцать лет — никто серьезно не занимался, ага.

                      Парадокс ФЯ конечно в том, что выучив этот подход, можно потом свободно применять его и в с++ и в с# и на java.

                      Можно, только неудобно. Мне после F# некоторые вещи в C# кажется совершенно избыточными.


                      1. knagaev
                        10.10.2015 20:06

                        А вы на самом деле используете в работе F#?
                        Я пока только изучаю его, очень нравится, даже не столько сам язык, сколько приёмы, которые можно сделать на нём.
                        Наверняка это общее свойство ФП языков — красивые приёмы, просто этот самый Скотт обладает хорошим качеством доходчиво донести идеи. И даже монады он лучше всех разъясняет имхо. Если соберусь, переведу цикл его статей про монады.
                        Но вот никак не могу понять, пригодится мне в работе F# или останется просто академическим опытом, который несомненно изменяет стиль программирования.
                        Что скажете?


                        1. lair
                          10.10.2015 20:08

                          А вы на самом деле используете в работе F#?

                          В работе — нет, в pet projects — да.


                          1. knagaev
                            10.10.2015 20:27

                            Что такое pet projects?bv


                            1. lair
                              10.10.2015 20:29

                              Маленькие проектики для себя.


                              1. knagaev
                                10.10.2015 20:31

                                А я в роли таких проектиков использую spoj. Или они более практичны?


                                1. lair
                                  10.10.2015 20:37

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


                                  1. knagaev
                                    10.10.2015 20:39

                                    Нет-нет, я понял :)
                                    Я имел в виду, что я для развлечения пишу на spoj (тоже проекты ведь), а у вас они чем практичны, то есть, какая польза от них?


                                    1. lair
                                      10.10.2015 20:45

                                      Что-то решает мои личные временные нужды, чем-то решаю задачи в образовательных курсах.


          1. webkumo
            09.10.2015 23:25
            +1

            Поинт статьи, исходный, стартовый «строгий язык упрощает понимание». В дальнейшем в статье этот поинт перефразировали в «строгий язык даёт больше шансов на простое понимание».
            Мой поинт — выполнение соглашение и возможность их реализации в языке без костылей и инородностей — упрощает понимание.
            Самый строгий язык не защитит от бардака, если писать наплевав на все стандарты. Все эти общие примеры кода на той же Scala подаваемые под соусом «смотрите насколько всё понятно» я вообще плохо воспринимаю — нет для меня там ничего очевидного. Про каррирование — вообще молчу.
            Но даже на c++ вполне можно писать читаемые программы.
            А с некоторыми моментами статьи я в принципе не согласен: например возвращение «статус-объекта» в котором ещё надо разбираться — приехал ответ, или пусто… например в Java при корректном описании контракта метода с использованием JSR-305 удобней возвращать null. Там сам компилятор, насколько я помню, предупредит о подозрительных местах — не проверенных nullable, лишних проверках notnull контактов (правда всё-же скомпилирует).

            В целом эта статья очень напоминает проблему checked/unchecked исключений в Java — вроде хорошая штука, эти checked исключения, но в некоторых местах (работа с БД, например) — лучше бы их не было… всё равно корректно отработать их невозможно… и в такой ситуации вполне корректно уронить приложение, пока дров не наломали.

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


            1. ImLiar
              10.10.2015 10:48
              +2

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


              1. webkumo
                10.10.2015 13:28
                +2

                Про perl программисты на нём пишушие тоже всегда заявляли, что он «легко читается»… но с ними, почему-то, мало кто соглашается.
                Я не говорю, что скала плохой язык — возможно потратив существенное время на изучение+привыкание всё это будет действительно читаться легко, но это не базовое свойство языка. И фразы «смотри, как легко читается» в постах рекламирующих этот язык действительности не соответвуют. Всё-таки эти посты, обычно, адресованы массовой публике, язык не знающей.


                1. ImLiar
                  11.10.2015 10:49

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


        1. VoidEx
          09.10.2015 22:37
          +4

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


      1. alexeibs
        10.10.2015 02:00

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


        1. lair
          10.10.2015 02:05

          «
          «reasoning about the code» means that you can draw conclusions using only the information that you have right in front of you, rather than having to delve into other parts of the codebase. In other words, you can predict the behavior of some code just by looking at it. You may need to understand the interfaces to other components, but you shouldn't need to look inside them to see what they do.

          [...]

          But this post is not about writing safe code, it's about reasoning about the code. There is a difference.
          »


          1. alexeibs
            10.10.2015 02:58

            In this post, I'll show you some of the issues that these design decisions cause, and suggest some ways to improve the language to avoid them.

            и еще
            Of course, there is a huge amount of advice out there on how to do just this: naming guidelines, formatting rules, design patterns, etc., etc.

            But can your programming language by itself help your code to be more reasonable, more predictable? I think the answer is yes, but I'll let you judge for yourself.

            И далее недостатки «плохих» языков и комментарии о том, как «хороший» язык должен реагировать на плохой код. Т.е. речь все-таки об отлавливании ошибок компилятором. Может быть автор и хотел сделать упор на читабельности кода — reasoning about the code в его терминах, но в итоге статья совершенно о другом получилась. К тому же, если какой-то код называется плохим, то неплохо было бы привести пример хорошего reasonable кода на «правильном» языке.


            1. VoidEx
              10.10.2015 09:13

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


              1. alexeibs
                10.10.2015 13:00
                +1

                Ок, это не ошибка. Но автор хочет, чтобы язык этого не разрешал, т.е. чтобы это было ошибкой с точки зрения компилятора, разве нет?


                1. qw1
                  10.10.2015 13:08

                  Может такая конструкция плохая с фунциональной точки зрения, но в других парадигмах жизнеспособна.

                  TransactionManager.TransferMoney(account1, account2, sum);

                  Тут подразумевается изменение объектов account1, account2, и это хорошо понятно из названия метода. Просто не надо называть методы DoSomething.


                  1. knagaev
                    10.10.2015 20:33

                    А с какого на какой счёт переводится sum? ;)


                    1. qw1
                      10.10.2015 20:38

                      ФП языки это как-то решают? Если отказаться от изменяемости, из записи

                      (account1, account2) = TransactionManager.TransferMoney(account1, account2, sum);

                      тоже непонятен этот момент.


                      1. knagaev
                        10.10.2015 20:49

                        Да я насчёт как надо называть методы.
                        Тогда может лучше TransferMoneyFrom (account, toAccount, sum)?


                        1. vintage
                          10.10.2015 21:17

                          Лучше так: [ TransactionManager Transfer money:sum from:account1 to:account2 ]
                          Это ObjC, если что.


                1. VoidEx
                  10.10.2015 15:17

                  Да, и так как в статье сравниваются разные языки с разным пониманием того, что считать ошибкой, то, по-моему, утверждение о том, что речь в ней «об отлавливании ошибок» — некорректно. Речь там не об отлавливании, а о том, что считать ошибкой, а что — нет, в контексте того, упростит ли это reasoning about the code.


                  1. alexeibs
                    10.10.2015 17:42
                    -1

                    Ок, но статический анализ кода делает тоже самое. Можно считать это ограничением возможностей языка. А юнит-тесты так вообще упрощают этот самый reasoning. Это если вернуться к моему первому комментарию, который был успешно заминусован.


                    1. lair
                      10.10.2015 17:44

                      А юнит-тесты так вообще упрощают этот самый reasoning.

                      Каким же образом?

                      (если помнить определение, данное автором статьи)


                      1. alexeibs
                        10.10.2015 17:47

                        Ну да, он говорит о «using only the information that you have right in front of you». Но зачем себя так ограничивать?


                        1. lair
                          10.10.2015 17:53
                          +2

                          Потому что это наименее ресурсоемко.


                          1. alexeibs
                            10.10.2015 21:21

                            О каких ресурсах идет речь?


                            1. lair
                              10.10.2015 21:26
                              +1

                              Читающего.


                    1. VoidEx
                      10.10.2015 18:06
                      +1

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


                      1. webkumo
                        10.10.2015 20:49

                        Стат.анализатор нужен не для чтения кода инлайн, он нужен для гарантий качества кода. А уже гарантии качества кода дают возможность читать инлайн с большим пониманием процесса.


                      1. alexeibs
                        10.10.2015 21:21

                        Статический же анализатор надо еще запустить

                        Компилятор/интерпретатор тоже нужно запускать. Все это легко автоматизируется и проблемы в этом нет.


                        1. VoidEx
                          10.10.2015 21:38

                          Ну, всё-таки предположение «если код на языке компилируется» более общее, нежели «если он ещё и проходит проверку стат анализатором». Если я ставлю какой-то пакет, я могу быть уже уверен, что оно хотя бы должно компилироваться. Может, для других языков принято ещё и указывать, проверку какими статическими анализаторами они проходят, но я о таком не слышал.


            1. lair
              10.10.2015 17:02

              Вот описанные им issues и влияют на чтение кода в первую очередь. И да, они следуют из дизайна языка.


    1. VoidEx
      09.10.2015 22:36
      +5

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


  1. Regis
    10.10.2015 00:48

    Эх, еще бы в язык, в котором бы можно было делать именованные синонимы ко встроенным типам либо как-то еще передавать контракты — цены бы ему не было.

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

    1)
    В объекте «книга» делаем поле isbn типа String, а то, что этот тип особенный — держим в уме (что, естственно, плохо). Там где ожидается передача ISBN — передаем строку (и не имеем гарантий, что туда не подсунут что-то левое).

    class Book { String isbn; }
    


    2) Делаем тип IsbnType, который внутри содержит поле stringValue типа String. В объекте «книга» делаем поле isbn типа IsbnType. Проблемы: мы не можем непосредственно к IsbnType применять строковые операции, а также, что хуже, такое решение обычно намного более тяжеловесное — ведь у нас IsbnType самостоятельный объект.
    class IsbnType { String value; }
    class Book { IsbnType isbn; }
    


    3) Делаем тип IsbnType, который расширяет тип String. В объекте «книга» делаем поле isbn типа IsbnType. Основная проблема — во многих языках возможности расширять стандартные типы просто нет; а там где есть — это выключает для «расширеных» типов многие оптимизации. А ведь нам-то всего-лишь хотелось разделить произвольные строки и ISBN…
    class IsbnType extends String;
    class Book { IsbnType isbn; }
    


    1. knagaev
      10.10.2015 00:56
      +1

      Посмотрите вот этот цикл.
      Мне кажется, он как раз про то, о чём говорите.


    1. vintage
      10.10.2015 01:55
      +2

      Смотри, что покажу :-) http://dpaste.dzfl.pl/e07109504a1c


      1. Regis
        10.10.2015 23:01

        Да, выглядит как то, что нужо. Спасибо!
        Хороший повод познакомиться с D поближе )


  1. potan
    10.10.2015 00:49
    +1

    Проблему 3 Scala к сожалению не решает. За пол! года сам дважды сравнил String с Option[String].
    А лучше всего под указанные критерии, IMHO, подходит Rust.


    1. stepik777
      10.10.2015 01:55

      Только вместо запрета на изменение, он запрещает иметь другие ссылки на объект, если есть одна мутабельная ссылка на него. Это решает проблему описанную в пункте 5, она заключалась в том, что там было одновременно две ссылки на один объект, одна в HashSet, вторая в локальной переменной и из локальной переменной его можно было изменить. В данном случае Rust выдал бы ошибку компиляции.


  1. vintage
    10.10.2015 01:07
    +10

    Увлёкшись одной парадигмой человек просто не видит других решений.

    > Объекты с идентичным содержанием должны быть равными по умолчанию.

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

    > Сравнение объектов с разными типами должно вызывать ошибку времени компиляции.

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

    > Объекты всегда должны быть инициализированными до корректного состояния. Невыполнение этого требования должно приводить к ошибке времени компиляции.

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

    > После создания объекты и коллекции должны оставаться неизменными.

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

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

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


    1. VoidEx
      10.10.2015 09:23

      > Если вы возьмёте две коробки и положите в них одинаковый набор игрушек, то они не станут одной и той же коробкой с игрушками — это будет две разные коробки с одинаковым содержимым. Различать или нет такие объекты зависит от задачи.
      К слову, в языках, где нет мутабельности, различать их не имеет смысла, так что там такой вопрос не стоит. Дальнейшие же рассуждения про то, что эффективнее сравнивать ссылки — в немутабельном случае тоже применимы.

      > при возвращении мне нужно лишь проверить, что мне вернули объект эквивалентный тому, что я выдал
      Так это разве противоречит тому, что написано?

      > После заполнения каждой графы, анкета ещё не становится полностью валидной
      Если говорить об этом конкретном примере, то незаполненная, даже пустая, анкета — вполне валидна, она — незаполнена, это ж разное. Невалидное — это null в полях «имя», «фамилия» при наличии ещё и методов вида «ФИО()», который к ним обращается.


      1. vintage
        10.10.2015 12:02
        +2

        1. Когда вы отгружаете 10 одинаковых коробок, вы не можете отгрузить одну и сказать, что отгрузил все. Чтобы решить эту проблему приходится каждому объекту приписывать глобально уникальный идентификатор. Собственно это и есть протечка в нашей чудесной абстракции.
        2. Но если мы сравниваем ссылки, то можем и мутабельные структуры использовать. Речь о том, что иммутабельность не является в данном случае преимуществом, а не о том, что она хуже.

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

        Валидность — понятие относительное. null как раз и показывает, что поле не заполнено. Другое дело, что ошибка должна возникать не при обращении к null, а при попытке записать его в non-nullable контейнер. И плох тот язык, где все типы по умолчанию nullable.


        1. VoidEx
          10.10.2015 15:13
          +2

          > Когда вы отгружаете 10 одинаковых коробок, вы не можете отгрузить одну и сказать, что отгрузил все.
          Этот пример просто хорошо ложится на ООП, но он как-то надуман. Если у вас реально разные одинаковые коробки, то припишите им какой-то ИД разный, и вот они уже разные в чистом ФП, и никакой протечки тут нет, просто в мейнстримовых языках у каждого объекта и так есть ИД — ссылка на объект, но хорошое ли это умолчание?

          > Противоречит, автор предлагает кидать ошибку всегда при сравнении разных типов.
          Я, если честно, не очень понял, почему скутер и велосипед в том примере — обязательно разные типы. Ну пусть это будет АДТ (Algebraic data type), например.

          > Валидность — понятие относительное. null как раз и показывает, что поле не заполнено.
          Для этого есть всякие Optional (он же Maybe), если это действительно нужно. Вопрос в том же, хорошо ли умолчание, когда null есть почти у всего?


          1. vintage
            10.10.2015 15:23
            +1

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

            Супертип вполне можно рассматривать как АДТ его субтипов, разве нет?

            nullable — это и есть optional.


            1. VoidEx
              10.10.2015 15:42

              Какой же это костыль, когда именно так и делается везде, просто везде такой костыль намертво приварен, который ещё и не всегда работает (ибо есть всякие value-типы).

              > Супертип вполне можно рассматривать как АДТ его субтипов, разве нет?
              Ну, есть весомые различия. Субтипы можно добавлять где-то отдельно, а АДТ надо по месту. В данном же контексте — два объекта одного АДТ — один тип, их сравнение — вполне нормальная операция. Т.е. и автор доволен, что нельзя сравнивать объекты разных типов, и мы, что можно сравнить велосипед и скутер.

              > nullable — это и есть optional
              Ну, да, речь об умолчании же. Что лучше — optional по умолчанию или специальным образом оговоренный?


              1. vintage
                10.10.2015 17:04

                Он намертво приварен просто потому что физическая реальность такая. Две коробки — это две коробки, даже если на них написать одинаковые идентификаторы.

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

                Я уже превентивно ответил на этот вопрос.


                1. VoidEx
                  10.10.2015 17:59
                  +2

                  А причем тут физическая реальность? Давайте лучше возьмем математику, вроде как неплохой инструмент при описании реальности. Вот две единичные матрицы — они одна или две разные?


                  1. vintage
                    10.10.2015 18:22

                    При том, что программирование — это не математика (формирование одних знаний из других знаний), а инженерия (применение знаний, для решения прикладных задач).

                    > они одна или две разные?

                    Вы сами ответили на свой вопрос:

                    > Вот две единичные матрицы


                    1. VoidEx
                      10.10.2015 18:34
                      +3

                      > программирование — это не математика
                      Ну а вот homotopy type theory — математика, и язык программирования заодно. Почему для инженерии язык на основе математики хуже?

                      > Вы сами ответили на свой вопрос
                      Нет, я спросил. Какой смысл в, например, различении одного числа 5 и «другого»? То, что их кладут в переменные, а потом про вопросе «есть два числа 5, они разные?» говорят, что таки разные, по-моему, пример телеги впереди лошади.


                      1. vintage
                        10.10.2015 21:11

                        Теория не может быть языком. Давайте завязывать с этим словоблудием?

                        Если вы берёте _два_ числа, то да, они не идентичны друг другу, иначе вы бы не смогли досчитать до двух. Равны ли они — зависит от отношения эквивалентности, которое вас в данный момент интересует.

                        Объясню по проще, пусть у нас есть запись:
                        a = 5
                        b = 5.0 + 0 * i

                        a и b эквивалентны, так как ссылаются на одну и ту же математическую сущность, но не идентичны, так как имеют разные имена и даже разные типы.


                        1. knagaev
                          10.10.2015 21:20
                          +4

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


                          1. vintage
                            10.10.2015 22:53

                            1. Я их смогу отличать
                            select from persons where name = 'Вася Пупкин' limit 1 offset 0
                            select from persons where name = 'Вася Пупкин' limit 1 offset 1

                            2. У суррогатного ключа есть неоспоримый плюс — он никогда не меняется в отличие от естественного.

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

                            4. Вы когда картошку покупаете, вы на каждой картофелине штрих код видите?


                            1. knagaev
                              10.10.2015 23:24
                              +3

                              1. В теории реляционных БД порядок записей не определён до выполнения оператора order by.
                              2. Это я даже не понял.
                              3. О чём и речь — и именно поэтому порядковый номер, если он не является свойством коробки, как объекта реального мира, не может быть использован для идентификации объекта. Потому что это чистый произвол.
                              4. Меня не интересует идентификация картофелины.


                              1. webkumo
                                10.10.2015 23:37

                                Хм, а я тогда тоже присоединюсь.
                                1. Это мешает различать записи?
                                2. Номер пенсионного… не помню как эта зелёная карточка называется — неизменен, в отличие
                                от ФИО, серии/номера паспорта,… По крайней мере он задуман как неизменнный.
                                3. Ну а equals в Java/C# выполняет семантически-зависимую роль. Т.е. если у нас есть нобходимость считать, что объект с одинаковыми свойствами идентичен другому (пример с числом где-то дальше в тредике) — то equals принудительно определяется по этим свойствам, в противном случае используется дефолтовое значение — равенство по конкретному инстансу (коробке).


                                1. knagaev
                                  11.10.2015 00:30
                                  +3

                                  1. В теории реляционных БД нет операторов limit и offset, кортеж может идентифицироваться только по значениям атрибутов.
                                  2. Так в чём плюс неизменности номера пенсионного? Особенно если учесть, что он никак не привязан к объекту, кроме произвольной договорённости.
                                  3. Вам ничто не мешает в таком случае завести два и более объектов, соответствующих одному объекту реального мира. Опять же в реляционных БД для отношения (моделирующего объекты реального мира) с искуственным первичным ключом обязательно должен быть определён альтернативный уникальный ключ. Иначе неминуемо нарушение целостности и коллизии.


                              1. vintage
                                11.10.2015 01:04
                                -2

                                1. Не гарантирован какой-то конкретный порядок, но он вполне себе определён реализацией и стабилен.

                                2. Никогда не задумывались, почему использование естественных первичных ключей является антипаттерном? http://dic.academic.ru/dic.nsf/ruwiki/915680

                                3. Как это не может, если может? Это первая коробка, а это вторая точно такая же.

                                4. О том и речь. Отношений эквивалентности много разных. А отношение идентичности одно и оно гарантирует эквивалентность по любому отношению.


                                1. knagaev
                                  11.10.2015 01:15

                                  1. Это откуда вы взяли, что стабилен? Даже по вашей ссылке из п.2 цитата

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

                                  2. А вы задумывались, что я не говорил, что суррогатные ключи — это зло?
                                  3. Что мешает назвать первую коробку второй, а вторую первой?
                                  4. Какое это имеет отношение к картофелинам?


                                  1. vintage
                                    11.10.2015 01:34
                                    -1

                                    1. Я не вижу причин ему быть не стабильным. Разве что там применяется метод Монте-Карло для поиска :-)

                                    2. А я и не этот тезис опровергал.

                                    3. Абсолютно ничего. Важно лишь, что при любом способе нумерации, они будут иметь разные номера.

                                    4. Такое, что любая из них эквивалентна любой другой (взаимозаменяемость), но не является ею (идентичность).


                                    1. lair
                                      11.10.2015 01:48
                                      +3

                                      Я не вижу причин ему быть не стабильным.

                                      И тем не менее, в MS SQL он может меняться в зависимости от индексов, созданных на таблице, и конкретных выбираемых полей.


                                      1. vintage
                                        11.10.2015 02:15

                                        Это разумеется, но один и тот же запрос по одним и тем же данным даёт один и тот же результат, а не каждый раз в новом порядке.


                                        1. lair
                                          11.10.2015 02:19
                                          +1

                                          … до тех пор, пока кто-то не поменял индекс на таблице. Ну и да, запросы «дай мне id» и «дай мне id и name», казалось бы, не должны возвращать данные в разном порядке… но возвращают. Могут возвращать.


                                        1. knagaev
                                          11.10.2015 10:34

                                          Это может зависеть от оптимизационных алгоритмов СХД.


                        1. VoidEx
                          10.10.2015 21:58
                          +2

                          > Теория не может быть языком.
                          Почему? Вы с HoTT знакомы?

                          > a и b эквивалентны, так как ссылаются на одну и ту же математическую сущность, но не идентичны
                          Именно эквивалентны, а не равны? Что вы подразумеваете под эквивалентностью? Насколько я знаю, с позиции какой-либо теории типов, для эквивалентности типы должны совпадать, разве нет? А разные они потому, что это — переменные. Т.е. надо держать в уме эту разницу между значениями и переменными, это просто разные вещи.
                          Давайте лучше другую запись, и на Хаскеле:

                          a = 5
                          b = a
                          

                          Здесь a и b — одно и то же. Т.е. внутри они вообще говоря могут ссылаться на разные участки памяти с одинаковым содержимым, но для рассуждений о поведении программы это не должно иметь и не имеет значения. У них один тип и одинаковое значение. Т.е. не надо держать в голове, что есть значения, а есть переменные, мы рассуждаем только о значениях. Т.е. нет никаких двух разных пятёрок, число 5 — это число 5.

                          do
                            a <- newMVar 5
                            b <- readMVar a >>= newMVar
                          

                          А вот тут a и b имеют тип MVar Int, и вот они не идентичны (a == b ? False), но их значения — равны. Вот тут у нас и впрямь переменные, что и выражено в их типах.


  1. ffriend
    10.10.2015 02:14
    +6

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

    var x = 2;
    DoSomething(x);
    
    // Каково значение y? 
    var y = x - 1;
    

    Основная проблема здесь даже не в том, что `x` меняет тип, а в том, что `DoSomething()` меняет `x`. Это неочевидно и вносит непредсказуемость? Да, отчасти. И тем не менее, это абсолютно стандартный паттерн в C и всех библиотеках, направленных на высокую производительность. В качестве примера, попробуйте написать библиотеку для обработки изображений, в которой функции каждый раз возвращают новую картинку вместо модификации той, которую ей передали в качестве параметра.

    Для смены типа аргумента функции я так и не смог придумать примера — слишком уж он надуманный — но в целом динамическая типизация позволяет безболезненно имплементировать в языке `eval()`, делать mock объекты без влезания в байткод, да и вообще гораздо приятней для динамического и метапрограммирования.

    То же самое касается неявного преобразования типов: если даже вам не нравится, что `1` может использоваться вместо `true`, то наверняка вы не имеете ничего против того, что `int` можно передавать в функцию, у которой в сигнатуре стоит `float`. А некоторые языки (например, OCaml) и это запрещают. Но, опять же, в OCaml приоритет на надёжность, так, чтобы можно было программировать самолёты и атомные станции, а большинство языков программирования вполне себе позволяют такое вот послабление системы типов.

    И так, в общем-то, можно пройтись по каждому из ваших примеров. Вы говорите, что `Equals()` должен возвращать `true` для одинаковых объектов. А что вообще значит одинаковые объекты? В Common Lisp, например, есть штук 5 функций сравнения двух значений, ранжирующихся от «одинаковы по сути» до «тот же объект». Это Common Lisp просто укуренный? Ну не знаю, а как по вашему, что должно возвращать выражение `1 == 1.0`? Суперстроготипизированный Haskell, кстати, возвращает `True`.

    Про неизменяемые коллекции я уже говорил — если сделаете эффективную библиотеку обработки изображений в функциональном стиле, можете смело писать кандидатскую. Про неизменяемость объектов тоже спорно: попробуйте, например, сделать неизменяемым объект типа `Connection`, у которого по определению есть изменяемый флаг «состояние».

    Пытаться избавиться от `null` — дело благородное, а вот от `exception` — не только дурное, но ещё и бесполезное: попробуйте, например, избавиться от исключения деления на ноль, возникающего из-за ограниченной точности представления чисел. Вот здесь (кстати, страничка про Haskell, откуда все алгебраические типы и защищённые вычисления пошли) описана ещё целая пачка исключений, которые невозможно предугадать или предупредить.

    Если очень хочется поиграть в проверяемыми исключениями, которые должны обязательно описываться в сигнатуры, поиграйте с Java — там всё именно так и реализовано. Только вот никто этим не пользуется — либо всё оборачивается в `RuntimeException`, который не проверяется, либо бросается просто обобщённый `Exception`, просто чтобы не писать по 5-8 бесполезных слов после каждой функции.

    Кстати, вот этот пример:
    // обработать оба случая
    if (customerOrError.IsCustomer)
        Console.WriteLine(customerOrError.Customer.Id);
    
    if (customerOrError.IsError)
        Console.WriteLine(customerOrError.ErrorMessage);
    

    совсем не кошерный по меркам функционального программирования. Вместо того, чтобы на каждом уровне проверять все возможные варианты, в ФП линии ML (т.е. Haskell, OCaml, Scala, F#) принято оборачивать значение в монаду и передавать её по всей цепочке вызовов — если монада хранит нормальное значение (кастомера), то к нему будут применяться преобразования, если ошибку — то ошибка будет просто продвигаться вперёд.

    Кстати, в C# появился оператор `?`, который, по-сути, выполняет ту же роль:

    int? length = customer?.Length;
    


    вместо

    int? first = (customers != null) ? customer.Length : null;
    



    1. beeruser
      10.10.2015 02:37
      +2

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

      Например на GPU обработка изображений именно так и работает.
      К тому же результат может иметь иной формат/размер.


      1. ffriend
        10.10.2015 03:16

        Не уверен, что вы хотели сказать, но что на CUDA, что на OpenCL ядра работают именно через модификацию выходного буфера:

        __kernel void sum(__global const float *a,  
                             __global const float *b,
                             __global float *c)                     // выходной буфер
        {                                                
              int gid = get_global_id(0);   
              c[gid] = a[gid] + b[gid];        
        }
        

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


        1. beeruser
          10.10.2015 08:02
          +3

          Своим примером вы подтвердили мои слова. Входные буферы константны и не пишутся, а выходной буфер пишется, но не читается. Т.е. это чисто функциональный подход.
          Альязить буферы (c (RW) == b (RO)) в данном примере можно, но работоспособность может не гарантироваться.

          Естественно никто в здравом уме не будет выделять каждый раз новый буфер в памяти. Для этого используется «пинг-понг» и пулы буферов. Однако это детали реализации абстракции «каждый раз новый буфер».


          1. ffriend
            10.10.2015 11:30

            Входные буферы константны и не пишутся, а выходной буфер пишется, но не читается. Т.е. это чисто функциональный подход.

            Буфер `c` передаётся извне и модифицируется, ровно так же, как в примере с `DoSomething`. Если вы хотите пример с RW переменными внутри одной функции — весь API стандарта BLAS (и его реализаций cuBLAS и clBLAS) к вашим услугам. Например, умножение двух матриц в BLAS выглядит так (в версиях для GPU больше параметров, но суть та же):

            gemm('N', 'T', alpha, A, B, beta, C)
            


            что читается как «alpha * нетранспонированную(A) * транспонированную(B) + beta * С; записать результат обратно в C».

            Однако это детали реализации абстракции «каждый раз новый буфер».

            «Каждый раз новый буфер» означает, что вы можете сохранять полученное значение сколь угодно долго и оно никогда никем не будет модифицированно. Пул буферов предполагает ровно противоположное — буфер будет переиспользован и его значение будет изменено. Так что я опять не понял, где вы тут увидели немутабельность, хоть на абстрактном, хоть на конкретном уровне.


            1. vintage
              10.10.2015 12:11

              Скажем так, вы вполне можете программировать в терминах «создаём новый буфер как функцию от старого», а компилятор может заметить что старый не используется и оптимизировать до «внести изменения в старый буфер и выдать его за новый».


              1. qw1
                10.10.2015 13:27

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

                buffer = putpixel(buffer, x, y, red);
                if (IsBufferFilled(buffer)) { buffer = putpixel(buffer, x+1, y, red); }

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


                1. qw1
                  10.10.2015 13:31

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


                  1. vintage
                    10.10.2015 15:15

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

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


                    1. qw1
                      10.10.2015 18:25

                      >> В функциональных языках вы не можете просто так «положить куда-то»

                      В идеальном мире — да, а в реальном много чего может быть. Например, запуск нового потока для контрольной записи буфера в файл, с передачей buffer ему без каких-либо предосторожностей. Какой-то программист подумает: «Все типы неизменяемые, можно не заморачиваться с копированием». А другой программист подумает: «Все функции чистые, если объект не используется дальше, можно его не копировать, а изменить»

                      >> универсально скомпилировать

                      Задать невидимый пользователю параметр — копировать или изменять буфер, потому что это знает только вызывающий код. Да, это выход


                1. VoidEx
                  10.10.2015 15:37

                  В чистых языках (а, я так понимаю, речь о них, раз обсуждается «каждый раз создаём новый») IsBufferFilled не может никуда положить, ибо нет мутабельных переменных. Вообще говоря, они есть, конечно, но в таком случае тип у Buffer будет другой (отражающий тот факт, что он мутабелен), ну и тогда не надо будет прикрываться ширмой «каждый раз новый буфер».

                  В Clean для такого есть en.wikipedia.org/wiki/Uniqueness_type
                  В Хаскеле — ST и State, например.
                  Т.е. функции вида «взять буфер, вернуть новый» соединяем в одну большую такую функцию, а потом ей кормим буфер. Изнутри ему некуда «убежать», потому результат — один «новый» буфер. ST — если хотим таки работать с мутабельными переменными и массивами, но с гарантией, что итоговый результат — чистый. Можно комбинировать, разумеется, под капотом ST, чуть повыше — State.

                  Насколько это на самом деле оптимизируется, я врать не буду, не проверял.


                  1. qw1
                    10.10.2015 18:31

                    Трюк с unique мне понравился. Однако, проверка, что объект повторно не использован, происходит в compile-time или run-time?

                    Если наиболее строго, для compile-time, я думаю, придётся писать громоздкий код в стиле

                    buffer = putpixel(buffer, x, y, red);
                    (flag, buffer) = IsBufferFilled(buffer);
                    if (flag) { buffer = putpixel(buffer, x+1, y, red); }


                    чтобы компилятор получил новый unique после вызова IsBufferFilled, а не передавал использованный unique-объект в следующий putpixel.


                    1. VoidEx
                      10.10.2015 18:36

                      В compile-time, это особенность системы типов.


                      1. qw1
                        10.10.2015 19:03

                        Интересно, если unique-объект включен в какой-то другой объект как поле (как элемент списка, если терминах lisp), весь родительский объект/список тоже становится unique?
                        Иначе я не представляю, как это можно проконтроллировать без полной верификации всех путей выполнения (что в принципе невычислимая задача)


                        1. VoidEx
                          10.10.2015 21:35

                          Насчёт этого, к сожалению, не в курсе


              1. ffriend
                11.10.2015 00:12

                Давайте ближе к телу. Возьмём функцию `filter2D` из OpenCV, которая, грубо говоря, имеет сигнатуру:

                void filter2D(Mat src, Mat dst);
                

                Где `src` — исходная картинка, а `dst` — выходная матрица, в которую записывается результат (destination). Возможно ли переписать эту функцию в функциональном стиле, чтобы она не модифицировала `dst` и при этом не требовала выделения новой памяти? При этом не забываем, что функция библиотечная, так что вызываемые и вызывающий код компилируются в разное время и разными компиляторами.


                1. knagaev
                  11.10.2015 00:34

                  Содержание dst на входе имеет значение при выполнении filter2D?


                  1. ffriend
                    11.10.2015 00:44

                    В данном случае если и имеет, то не используется. Ниже в примере с `gemm` аналогичный параметр используется и как входная, так и выходная переменная.


                    1. knagaev
                      11.10.2015 00:50

                      Но тогда что мешает, как уже было неоднократно сказано, использовать заранее выделенные области внутри функции и возвращать их?


                      1. ffriend
                        11.10.2015 01:18

                        О каких заранее выделенных областях внутри функции идёт речь? О преаллоцированной статической переменной (в смысле C++) внутри фукнции? Автор функции не может знать, матрицу какого размера ему передадут. О «бесконечном пуле буферов»? Это то же самое, что просто доступ в RAM, и в RAM-е использованную память (свободные буферы) нужно когда-то освобождать.

                        Приведите, пожалуйста, пример кода функции преобразования массива, которая не принимает выходной буфер как параметр (т.е. не модифицирует переданные в неё параметры), и не выделяет новую память при каждом вызове.


                        1. knagaev
                          12.10.2015 11:24

                          Тут у вас конфликт в двух посылках (почти как у Печкина в Простоквашино :)):
                          1. Выделение памяти ведёт к снижению производительности
                          2. Написать в функциональном стиле без выделения памяти невозможно
                          Отсюда вывод: написать в функциональном стиле без снижения производительности невозможно.

                          Но здесь «выделение памяти» в этих двух пунктах может иметь разное значение.
                          Даже в самом кошерном ОО С++ если нужна сверхскорость при работе с объектами, начинают переопределять new() с использованием нестандартных аллокаторов.
                          Кто мешает написать функцию ФП с «выделением» памяти, которое на самом деле будет просто возвращать ссылку на область памяти из предвыделенного пула?


                          1. ffriend
                            12.10.2015 13:21

                            Вы опять всё сводите к какому-то абстрактному пулу предвыделенных буферов. Ну пусть будет у нас пул из 1000 буферов, пусть даже каждый буфер уже будет нужного размера — что угодно. Мы вызываем нашу функцию, результат записываем в буфер #1, затем в буфер #2, затем в буфер #3 и т.д. В конце концов буферы у нас заканчиваются, что будем делать в этом случае?


                            1. knagaev
                              12.10.2015 15:52
                              +1

                              Буферы так же будут освобождаться, когда отработают.
                              Можно вручную, а можно предусмотреть и автоматическую сборку мусора (которая, можно сказать, из FP родом).
                              Просто вы говорите, что выделение памяти на каждый вызов функции затормаживает обработку, поэтому надо выделять заранее буфер и всё время его передавать в функцию, чтобы сэкономить на операции выделения памяти.
                              А я говорю, что скорость выделения может быть очень высока и этим можно пренебречь.


                              1. ffriend
                                12.10.2015 16:30

                                Можно вручную, а можно предусмотреть и автоматическую сборку мусора (которая, можно сказать, из FP родом).

                                Т.е. мы снова приходим просто к менеджеру памяти без использования буферов вообще.

                                А я говорю, что скорость выделения может быть очень высока и этим можно пренебречь.

                                Пример на Julia:

                                # импортируем BLAS
                                julia> using Base.LinAlg.BLAS
                                
                                # создаём два исходных массива A и B плюс один буфер C
                                julia> A = rand(10000, 100); B = rand(100, 10000); C = zeros(10000, 10000);
                                
                                # перемножение матриц с выделением памяти
                                julia> @time for i=1:10 C = gemm('N', 'N', 1.0, A, B) end
                                  8.822950 seconds (13.02 k allocations: 7.451 GB, 6.15% gc time)
                                
                                # перемножение матриц с использованием преаллоцированного буфера
                                julia> @time for i=1:10 gemm!('N', 'N', 1.0, A, B, 0.0, C) end
                                  4.607092 seconds
                                

                                Не знаю как вы, но я бы не стал пренебрегать оптимизацией алгоритма на 92%.


                                1. knagaev
                                  12.10.2015 17:30

                                  «Выделение» памяти может быть без выделения.
                                  Ну вот, например, как здесь C++: Custom memory allocation
                                  (И это я ещё не вспоминаю о том, что автор статьи явно указал, что пишет не о проблемах производительности :))


                                  1. ffriend
                                    12.10.2015 19:57

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

                                    void filter2D(Mat src, Mat dst)
                                    


                                    в функцию

                                    Mat filter2D(Mat src)
                                    


                                    без необходимости динамически выделять память.


                                    1. knagaev
                                      12.10.2015 22:25

                                      Mat filter2D (Mat src)
                                      {
                                      Mat dst = MemoryManager.GetMat(sizeof(src));
                                      dst = map (src, filter2Dfunc);
                                      return dst;
                                      }


                                      1. ffriend
                                        12.10.2015 22:49

                                        Я начинаю подозревать, что вы меня троллите. Мы 16-й комментарий обсуждаем стратегии выделения памяти, а вы всю суть вопроса снова сводите к какому-то абстрактному классу. А что должно находиться внутри `MemoryManager.GetMat`?


                                        1. knagaev
                                          12.10.2015 23:05

                                          Да зачем мне вас троллить?
                                          Просто не знаю как уже донести эту мысль (которая здесь и другими участниками высказывалась), что для производительности можно выделять память из пула.
                                          Уж не напоминаю, что в самой статье тема производительности тоже затрагивалась, причём с однозначным ответом на этот вопрос.
                                          Внутри MemoryManager.GetMat может быть, например, кольцевой буфер.
                                          Или дерево с ссылками на свободные chunkи памяти.
                                          Или что угодно, что может оперировать с _предвыделенными_ областями памяти.
                                          Единственная задача — предоставить абстракцию, чтобы в этот логический блок памяти записывались данные только для матрицы dst, и не записывались никакие другие.
                                          Принципиальная реализация не важна.
                                          Вы лучше скажите почему «выделение» — это всегда выделение.
                                          Может я что-то неправильно понимаю в ваших словах.


                                        1. knagaev
                                          12.10.2015 23:07

                                          И ещё было бы интересно понять почему ни один из описанных аллокаторов не решает задачу.
                                          Там в конце даже тесты производительности, вполне достойные.


                                          1. ffriend
                                            13.10.2015 00:15

                                            Уж не напоминаю, что в самой статье тема производительности тоже затрагивалась, причём с однозначным ответом на этот вопрос.

                                            Это ветка ответа на мой первый комментарий началась с ответа beeruser относительно производительности, про тругие юз кейсы для модификации переменных и смены типа я ответил дальше (параграф про `eval()`).

                                            Внутри MemoryManager.GetMat может быть, например, кольцевой буфер.

                                            Который при достижении лимита либо начнёт перезаписывать старые значения, либо вообще откажется выделять новые объекты. Шикарный вариант.

                                            Или дерево с ссылками на свободные chunkи памяти.

                                            И повторить урезанную функциональность malloc-а. Действительно, давайте изобретём велосипед.

                                            Или что угодно, что может оперировать с _предвыделенными_ областями памяти.

                                            malloc и garbage collector могут работать с предвыделенными областями памяти. Иногда могут запрашивать у операционной системы дополнительную память, но большинство аллокаций происходит в предвыделенной области. Внезапно.

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

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

                                            Принципиальная реализация не важна.

                                            Если мы говорим про производительность, а мы уже выяснили, что в этом треде мы говорим про неё, то важен даже порядок записи в кеш процессора, а аллоцирование памяти — вообще один из первых пунктов оптимизации.

                                            Вы лучше скажите почему «выделение» — это всегда выделение.

                                            • Быстрое выделение памяти
                                            • Быстрое освобождение памяти
                                            • Общий случай

                                            Выберете 2 из 3. Если вы хотите быстро выделять память (например, просто увеличить указатель в куче в системах со сборщиком мусора), то придётся потретить время на её освобождение (сборка мусора). Если вы хотите быстро освобождать (пометить кусок памяти как свободный), то придётся потратить время на поиск подходящего куска в следущий раз (как это делает malloc). В некоторых специальных случаях можно получить и (1) и (2) (например, стек и выделяется, и уничтожается простым изменением указателя), но это налагает серьёзные ограничения (стек может либо расти, либо раскручиваться, сохранение и разрушение произвольных фреймов невозможно). Как я уже говорил, найдёте способ — пишите кандидатскую.

                                            И ещё было бы интересно понять почему ни один из описанных аллокаторов не решает задачу.

                                            Начнём с того, что каждый из этих аллокаторов требует предварительного выделения некоторого объёма памяти, какого именно ни создатели библиотеки, ни чаще всего даже пользователи библиотеки не знают. Вы можете выделить 1Gb и использовать 256К, а можете выделить 100Мб и через 3 минуты свалиться с нехваткой памяти.

                                            Кроме того:

                                            • Linear Allocator вообще не умеет освобождать буферы по одному, только очищать всю выделенную память сразу. Мне вообще сложно придумать, где он применим
                                            • Stack Allocator, как и обычный стек, не позволяет произвольного выделения памяти. Т.е. буфер изображения #3 мы сможем уничтожить не раньше, чем уничтожим буфер изображения #4
                                            • FreeList Allocator — по сути, тот же malloc, но без системных вызовов и в маленьком куске памяти. Результаты абсолютно бессмысленны: во-первых, используется метод first-fit вместе оптимального best-fit, во-вторых, сначала делается N аллокаций (константная операция, учитывая first-fit), а затем N деаллокаций (опять же, константная операция). Если попробовать выделять и освобождать память вперемешку и на большем объёме памяти, результаты будут как раз как у malloc
                                            • Pool Allocator работает только с буферами фиксированного размера. Так в документации и напишем: накладываем фильтры на изображение, но только если оно размера 96x96.


                                            Так что бесплатного сыра не бывает, пора бы это уже осознать.


                                            1. knagaev
                                              13.10.2015 00:34
                                              +1

                                              По поводу выделения гигабайта или 100 Мб — как поступают в реализации std::vector? Или там всегда известно какой длины массив туда собираются сохранить?
                                              То же и про кольцевой буфер.
                                              Конкретные реализации дают выигрыш в конкретных случаях.
                                              Про бесплатный сыр никто не говорил и не ожидал.
                                              Менторский тон заключительной фразы не способствует приятному обсуждению.


                                              1. ffriend
                                                13.10.2015 01:47

                                                По поводу выделения гигабайта или 100 Мб — как поступают в реализации std::vector?

                                                Я не пишу на C++ и не знаю, как там это реализовано, но в Java аналогичная структура — ArrayList — хранит массив с запасом, а при достижении границы переаллоцирует весь массив. К менеджерам памяти (или, если хотите, мендежеру буферов) такая схема неприменима, хотя бы потому что любое удаление элемента (т.е. освобождение памяти) требовало бы полного переписывания массива/преаллоцированного куска памяти.

                                                То же и про кольцевой буфер.

                                                Действительно, то же и про кольцевой буфер.

                                                Про бесплатный сыр никто не говорил и не ожидал.

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


                                                1. knagaev
                                                  13.10.2015 01:49

                                                  Ок-ок-ок, вы победили.


                                                1. VoidEx
                                                  13.10.2015 10:39

                                                  Ну «можно пренебречь» не значит, что он бесплатный, а какой стоимостью сыра можно пренебречь — зависит от задачи. Можно ж и обратно развернуть — мутабельность и прочие плюшки тоже не бесплатны — увеличивают «стоимость» разработки.


                                                  1. ffriend
                                                    13.10.2015 11:16

                                                    мутабельность и прочие плюшки тоже не бесплатны — увеличивают «стоимость» разработки.

                                                    Ну это тоже спорное утверждение: сравните, например, Haskell и Python — те, кто пробовал писать продакшн системы на обоих, говорят, что на Питоне таки быстрее ;)


                                                    1. VoidEx
                                                      13.10.2015 11:34

                                                      Продакшн я не писал, но активно писал (и пишу) SublimeHaskell — плагин для Sublime, ну и вот на Хаскеле — быстрее намного и проще :)
                                                      Добавил один конструктор — компилятор укажет все места, где надо что-то поменять. Поменял сигнатуру — так же.
                                                      В Питоне с этим дело обстоит несколько иначе. Обкладывать даже такие вещи тестами? Ну так это увеличивает время обнаружения ошибки на порядок и повышает время на их исправление.

                                                      Так что такое сравнение в лоб (вида «писал что-то похожее на одном и другом») — это всего лишь один элемент, необходимый для оценки, вывода по нему делать нельзя, ибо в наличии разные инструменты; влияют разные стили написания и разный background у пишущих.


                                            1. VoidEx
                                              13.10.2015 11:08
                                              +1

                                              Эта ветка началась, вроде как, вот с этого:

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

                                              А это далеко не общий случай, это очень частный случай. Возьмем, например, обработку списков. В цепочке обработок списка вида take n . filter f . map g и типа [a] -> [a] никаких списков не создаётся, к слову, и более того, даже обход будет — один, а не три (в ленивом языке), и обходить будет не весь список (в конце ж у нас take n); причём map g, например, может быть оформлено отдельной функцией, которая якобы принимает список и возвращает «новый» список.
                                              Для того же самого в императивном языке придётся придумывать итераторы или что-то типа того.

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


                                              1. knagaev
                                                13.10.2015 11:14

                                                Спасибо большое за этот пост!
                                                Вы отлично написали, просто не в бровь, а в глаз.
                                                Только сложно убедить того, кто не пользовался этим.
                                                Об этом писал Грэм в Beating the Averages про гипотетического программиста на Blub.


                                                1. ffriend
                                                  13.10.2015 11:35

                                                  Только сложно убедить того, кто не пользовался этим.

                                                  Если вы это про меня, то на функциональных языках я пишу года этак с 2008, причём года 3 писал продакшен системы на Лиспе, про который говорит в своей статье Грэм. Только нужно понимать, что любая фича любого языка появилась не просто так, и функциональные языки дают прирост скорости разработки только в своей небольшой области. Так же, как и императивные языки дают серьёзный выигрыш в своей области. А в большинстве задач вообще всё равно, какую парадигму использовать.


                                                  1. knagaev
                                                    13.10.2015 11:38

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


                                                    1. ffriend
                                                      13.10.2015 11:41

                                                      А, тогда простите, просто тот комментарий был ответом на мой.


                                                    1. whitepen
                                                      15.10.2015 15:03

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


                                              1. vintage
                                                13.10.2015 11:32

                                                Только вот наглядный код на Хаскеле тормозит, а оптимальный — ребус.
                                                http://geektimes.ru/post/261144/#comment_8759612


                                                1. VoidEx
                                                  13.10.2015 11:38

                                                  Это fibs-то — ребус?


                                                  1. vintage
                                                    13.10.2015 11:46

                                                    Да, и это уже на элементарном приветмире.


                                                    1. VoidEx
                                                      13.10.2015 11:51

                                                      Это не ребус для тех, кто сколь-нибудь программировал на Хаскеле.
                                                      Кому хочется орудовать состоянием в более сложных примерах, ну можно взять и State.

                                                      Я там выше упоминал take n . filter f . map g, как это переписать на императивный?


                                                      1. vintage
                                                        13.10.2015 12:16

                                                        Вы не поверите: [1,2,3,4,5,6,7,8,9].map!q{ a * 2 }.filter!q{ a % 4 }.take( 5 )


                                                        1. VoidEx
                                                          13.10.2015 12:20

                                                          Во-первых, теперь мой черёд говорить «ребус» :)
                                                          А во-вторых, там же наверняка не просто списки, я угадал?
                                                          Можно из этого всего выдрать маленький кусок в виде отдельной функции:
                                                          foo = map g
                                                          Как это будет выглядеть?


                                                          1. vintage
                                                            13.10.2015 12:59

                                                            auto g( Item )( Item a ) { return a * 2; } auto foo( Item )( Item a ) { return a.map!g; }


                                                            1. VoidEx
                                                              13.10.2015 13:04

                                                              Я с D знаком весьма поверхностно, потому у меня есть ещё вопросы.
                                                              Если я верно помню, то! — это применение шаблона.
                                                              Значит ли, что g обязана быть известна в момент компиляции?
                                                              В коде, который я написал не вам, чуть ниже, но на основе моего:

                                                              foo = take n
                                                              bar = filter f
                                                              baz = map g
                                                              quux = foo . bar . baz
                                                              

                                                              Проход по списку будет один. Верно ли будет это и в вашем примере, если аналогично дописать остальные функции, а потом их скомпоновать? Верно ли для обычного списка, или для только для итераторов?


                                                              1. vintage
                                                                13.10.2015 13:13

                                                                Да, забыл сказать, что map, filter и take возвращают те же ленивые итераторы, что и в хаскеле. http://dlang.org/phobos/std_algorithm_iteration.html

                                                                Да, g должна быть выводима во время компиляции.


                                                                1. VoidEx
                                                                  13.10.2015 13:44

                                                                  Спасибо.

                                                                  > Да, забыл сказать, что map, filter и take возвращают те же ленивые итераторы, что и в хаскеле.
                                                                  Вот только в хаскеле эти ленивые итераторы определяются так:

                                                                  data List a = Null | Cons a (List a)
                                                                  

                                                                  И так для любого своего типа данных.

                                                                  > Да, g должна быть выводима во время компиляции
                                                                  А так — нечестно :)

                                                                  Я веду к тому, что необходимость «ленивых итераторов» очевидна, и везде так или иначе реализуется. Но в Хаскеле не нужно делать это как-то особенно, там оно делается по умолчанию. А чистота даёт больший просто для высокоуровневых оптимизаций.
                                                                  Хотя, конечно, стоит отметить, что иногда излишняя ленивость становится источником проблем, когда неясно, где и в какой момент что-то вычисляется, а это может порой неслабо повлиять на поведение программы. Вот в этих случаях анализировать сложнее, но не так, чтобы очень.


                                                                  1. vintage
                                                                    13.10.2015 14:27

                                                                    > Вот только в хаскеле эти ленивые итераторы определяются так:

                                                                    Я не силён в Хаскеле, что тут происходит?

                                                                    > А так — нечестно :)

                                                                    Почему?

                                                                    > Но в Хаскеле не нужно делать это как-то особенно, там оно делается по умолчанию.

                                                                    Ленивые вычисления? В императивных языках тоже не особо принято вычислять то, что не требуется прямо сейчас.


                                                                    1. VoidEx
                                                                      13.10.2015 16:16

                                                                      Я не силён в Хаскеле, что тут происходит?

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

                                                                      Почему?

                                                                      Потому что в моём коде это не так.


                                                                      1. vintage
                                                                        13.10.2015 19:55

                                                                        Понятней не стало :-) Это бесконечный список из Null-ов или что?

                                                                        Значит его и фиг оптимизируешь толком


                                                                        1. VoidEx
                                                                          13.10.2015 22:03

                                                                          Понятней не стало :-) Это бесконечный список из Null-ов или что?

                                                                          Это просто список, т.е. либо пустой (Null), либо голова + хвост.

                                                                          Значит его и фиг оптимизируешь толком

                                                                          Ну как-то умудряются же, и чистота этому только способствует, потому что, например, map f . map g можно преобразовать в map (f . g) вне зависимости от того, что там за f и g.


                                                                          1. vintage
                                                                            13.10.2015 22:33

                                                                            Список чего? Или это абстрактный тип такой?

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


                                                                            1. VoidEx
                                                                              13.10.2015 23:28

                                                                              Список чего? Или это абстрактный тип такой?

                                                                              Полиморфный

                                                                              Преобразовать-то можно и в императивном языке

                                                                              Разумеется, нельзя, потому что в императивном языке эти две конструкции просто-напросто неэквивалентны.
                                                                              f потенциально может выводить на экран, g — тоже, а порядок менять негоже.


                                                                              1. vintage
                                                                                13.10.2015 23:50

                                                                                Может хватит говорить загадками? Что конкретно содержится в этом полиморфном списке?

                                                                                Почему это негоже? map возвращает ленивый итератор, а не результат, так что не гарантирует когда функции будут вызваны и будут ли вызваны вообще.


                                                                                1. VoidEx
                                                                                  14.10.2015 00:21
                                                                                  +1

                                                                                  Может хватит говорить загадками?

                                                                                  Куда уж конкретнее? Параметрический полиморфизм

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

                                                                                  Я специально написал «порядок». Если же map может применять функцию и в обратном порядке, то это уже не оптимизация.

                                                                                  d = {'x':0}
                                                                                  def foo(x):
                                                                                  	d['x'] = d['x'] + 1
                                                                                  	x = x + d['x']
                                                                                  	print(x)
                                                                                  	return x
                                                                                  
                                                                                  def foofoo(x):
                                                                                  	foo(x)
                                                                                  	return foo(x)
                                                                                  
                                                                                  def test1():
                                                                                  	d['x'] = 0
                                                                                  	return list(map(foo, [0,0,0]))
                                                                                  def test2():
                                                                                  	d['x'] = 0
                                                                                  	return list(map(foo, map(foo, [0,0,0])))
                                                                                  def test3():
                                                                                  	d['x'] = 0
                                                                                  	return list(map(foofoo, [0, 0, 0]))
                                                                                  


                                                                                  Что выведет test1()? Что выведет test2()? А что выведет test3()?


                                                                                  1. vintage
                                                                                    14.10.2015 00:47
                                                                                    -1

                                                                                    Я знаю, что такое полиморфизм, мне не понятен конкретный ребус, который вы привели.

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


                                                                                    1. ffriend
                                                                                      14.10.2015 01:31

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

                                                                                      Это не совсем так. Ленивый, он же нормальный порядок вычислений определён абсолютно строго, просто иначе, чем аппликативный. Например, при аппликативном порядке выражение `f(g(x))` будет вычисляться строго слева направо (сначала `x`, потом `g()`, потом `f()`), а при нормальном — сначала слева направо (вызовы функций), а потом слева направо («закрытие» функций). Но этот порядок всегда строго определён и вполне поддаётся анализу.


                                                                                      1. vintage
                                                                                        14.10.2015 08:22

                                                                                        Разумеется он строго определён и зависит от реализации f, g, x


                                                                                  1. ffriend
                                                                                    14.10.2015 01:24

                                                                                    У вас ужасный, ужасный, ужасный стиль программирования на Python. Как я уже говорил, если кто-то применяет функции с побочными эффектами в функциях типа `map`, то ему нужно отрывать руки с корнем, вне зависимости от языка.


                                                                                    1. VoidEx
                                                                                      14.10.2015 10:14

                                                                                      У вас ужасный, ужасный, ужасный стиль программирования на Python

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

                                                                                      Как я уже говорил, если кто-то применяет функции с побочными эффектами в функциях типа `map`, то ему нужно отрывать руки с корнем, вне зависимости от языка.

                                                                                      Ну вот print — вроде грязная функция? А В Хаскеле можно передавать :)
                                                                                      sequence_ $ map print [1..3] -- 1 2 3
                                                                                      sequence_ $ reverse $ map print [1..3] -- 3 2 1
                                                                                      


                                                                                      1. ffriend
                                                                                        14.10.2015 13:26

                                                                                        Ну вот print — вроде грязная функция? А В Хаскеле можно передавать :)

                                                                                        Кто ж виноват, что в Haskell нет `foreach` :D

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

                                                                                        Хм, вы имеете ввиду оптимизацию на уровне компилятора? Тогда я не очень понимаю её суть — насколько я понимаю, композитная функция `(f. g)` всё равно транслируется в машинный код как последовательное применение двух функций к каждому элементу. Или здесь можно ещё что-то соптимизировать?

                                                                                        Единственный раз, когда я видел полезность композиции функций для производительности, был в PySpark, где выражение (`rdd`, грубо говоря, — это большой-большой ленивый список, загружаемый с диска):

                                                                                        rdd.map(g).map(f)
                                                                                        


                                                                                        транслировалось в

                                                                                        rdd.map(compose(f, g))
                                                                                        


                                                                                        Но там это сделано из-за того, что каждый вызов к `.map` означает передачу данных между процессами Java и Python через диск, так что композиция функций уменьшает количество IO операций ровно вдвое. Это оптимизация на уровне PySpark, которая может произойти, а может по определённым причинам и не произойти. Но, опять же, от программиста ожидается, что он не будет стрелять себе в ногу и передавать в `.map` грязные функции, от порядка вызова которых может измениться результат.

                                                                                        Обратите внимание, что в этом случае ни Java (Scala), ни Python чистыми языками не являются.


                                                                                        1. VoidEx
                                                                                          14.10.2015 19:19
                                                                                          +1

                                                                                          Кто ж виноват, что в Haskell нет `foreach` :D

                                                                                          Отчего ж, есть и mapM и forM, но можно и так тоже.
                                                                                          Есть даже такое :)
                                                                                          ghci> mapMOf_ (each . _Right . each . _head) putChar [Right ["foo", "baz", "baz"], Left 10, Right ["x", "y", "zoo"]]
                                                                                          fbbxyz
                                                                                          

                                                                                          Взять каждый (each) элемент, затем пойти внутрь конструктора Right (_Right) (при этом откинув те, что Left), затем взять каждый элемент (each), и затем первую букву (_head)
                                                                                          И для них выполнить putChar
                                                                                          Ну да ладно, это так, к слову.

                                                                                          Хм, вы имеете ввиду оптимизацию на уровне компилятора? Тогда я не очень понимаю её суть — насколько я понимаю, композитная функция `(f. g)` всё равно транслируется в машинный код как последовательное применение двух функций к каждому элементу. Или здесь можно ещё что-то соптимизировать?

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

                                                                                          Вот, например, из документации:
                                                                                          So, for example, the following should generate no intermediate lists:
                                                                                          array (1,10) [(i,i*i) | i <- map (+ 1) [0..9]]
                                                                                          



                                                                                          К сожалению, глубоко тему не рыл, но вот, например: www.randomhacks.net/2007/02/10/map-fusion-and-haskell-performance
                                                                                          Стоит также отметить, что если в случае итераторов всё понятно, они специально так заделаны, то с двумя подряд foreach так уже сделать нельзя:
                                                                                          for x in xs:
                                                                                            f(x)
                                                                                          for x in xs:
                                                                                            g(x)
                                                                                          
                                                                                          # Совсем не то же самое, что:
                                                                                          # for x in xs:
                                                                                          #   f(x)
                                                                                          #   g(x)
                                                                                          

                                                                                          А если мы хотим, чтоб можно было — придется переезжать на итераторы, тогда как в хаскеле что foreach, что «итераторы» делаются одной конструкцией.

                                                                                          Написал несколько сумбурно, но, надеюсь, донес мысль :)


                                                                            1. mird
                                                                              15.10.2015 12:21

                                                                              Это типичный функциональный список. Он же односвязный список. Представляет собой всегда голову (то есть первый элемент и хвост — такой же список только без первого элемента). По идее, он генерик, какого реально типа определяется type infering по использованию.


                                                                              1. vintage
                                                                                15.10.2015 12:46

                                                                                Кажется я разгадал этот ребус :-)

                                                                                class List( Item ) {
                                                                                Item value;
                                                                                List!Item next;
                                                                                }


                                                                                1. mird
                                                                                  15.10.2015 12:52

                                                                                  class FunctionalList<T>
                                                                                  {
                                                                                        T Head{get;set;}
                                                                                        FunctionalList<T> Tail{get;set;}
                                                                                  }
                                                                                  


                                                                                  Но без паттерн матчинга работать с ним не удобно.


                                                                                1. VoidEx
                                                                                  15.10.2015 13:17

                                                                                  Нет, уж скорее

                                                                                  class List
                                                                                  class Null: List
                                                                                  class Node: List

                                                                                  Ибо ваше на хаскель переводится так:

                                                                                  data List a = List (Maybe a) (Maybe (List a))
                                                                                  Что совсем не то же самое


                                                                              1. VoidEx
                                                                                15.10.2015 13:20

                                                                                Там либо пусто, либо голова + хвост, так что фраза «представляет собой всегда голову» неточна.


                                                                                1. mird
                                                                                  15.10.2015 13:54

                                                                                  Это с точки зрения функционального языка и паттернматчинга принципиально.


                                                            1. vintage
                                                              13.10.2015 13:06

                                                              Или так:
                                                              const g = q{ a * 2 };
                                                              alias foo = map!g;


                                                      1. ffriend
                                                        13.10.2015 12:25

                                                        Я там выше упоминал take n. filter f. map g, как это переписать на императивный?

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

                                                        [g(x) for x in a if f(x)][:n]
                                                        


                                                        Если вам больше по душе вид с функциями, то можно ещё и так:

                                                        from itertools import *
                                                        islice(ifilter(f, imap(g, a)), n)
                                                        


                                                        Хотя, конечно, более кашерный подход с точки зрения Python будет вынлядеть так:

                                                        from itertools import *
                                                        r = imap(g, a)
                                                        r = ifilter(f, r)
                                                        r = islice(r, n)
                                                        

                                                        Префикс `i`, кстати, говорит о том, что мы работаем с итераторами, т.е. теми же самыми ленивыми списками.


                                                        1. VoidEx
                                                          13.10.2015 12:33

                                                          > А вас какая часть этого выражения интересует? Функции работы со списками, каррирование или композиция функций?
                                                          Композиции и возможность записывать куски композиции отдельно, т.е. типа такого:

                                                          foo = take n
                                                          bar = filter f
                                                          baz = map g
                                                          quux = foo . bar . baz
                                                          


                                                          > т.е. теми же самыми ленивыми списками
                                                          Да. При этом в чистом языке можно подряд идущие filter f . map g сворачивать в использование одной функции (Rewrite rules), потому что ни f, ни g не производят побочных эффектов.
                                                          А что если у нас не списки, а деревья? Как сделать всё то же, но для своего типа данных?


                                                          1. ffriend
                                                            13.10.2015 13:05

                                                            import functools
                                                            from functools import *
                                                            from itertools import * 
                                                            
                                                            def itake(n, coll): return islice(coll, n)
                                                             
                                                            foo = partial(itake, 3)
                                                            bar = partial(ifilter, lambda x: x > 2)
                                                            baz = partial(imap, lambda x: x + 1)
                                                            
                                                            def compose(*functions):
                                                                return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, lambda x: x)
                                                            
                                                            quux = compose(foo, bar, baz)
                                                            list(quux([1, 2, 3, 4, 5, 6]))  # ==> [3, 4, 5]
                                                            


                                                            Да. При этом в чистом языке можно подряд идущие filter f. map g сворачивать в использование одной функции (Rewrite rules), потому что ни f, ни g не производят побочных эффектов.

                                                            Я ничего не знаю про Rewrite rules, но ни одна из перечисленных функций не обладает побочными эффектами.

                                                            А что если у нас не списки, а деревья? Как сделать всё то же, но для своего типа данных?

                                                            По деревьям вы тоже итерируете последовательно? Тогда добавьте интерфейс итератора к вашему объекту (переопределите некоторые специальные функции) и сможете итерировать по ним.


                                                            1. VoidEx
                                                              13.10.2015 13:23
                                                              +1

                                                              Уж лучше идиоматично через list comprehensions, а то так и нечитаемее, и тормознее. А если идиоматично, то уже bar оттуда не вычленить так просто, композиционность страдает.

                                                              > Я ничего не знаю про Rewrite rules, но ни одна из перечисленных функций не обладает побочными эффектами.
                                                              Как же это не обладает, когда _может_ обладать (у меня там недаром f и g — произвольные функции), а это для оптимизатора весьма важно.

                                                              > По деревьям вы тоже итерируете последовательно?
                                                              Можно даже бесконечные деревья (как и списки) строить. Необязательно последовательно. Те же самые filter, map, что-либо ещё.

                                                              > Тогда добавьте интерфейс итератора к вашему объекту (переопределите некоторые специальные функции) и сможете итерировать по ним.
                                                              Итератор ходит вперёд. По дереву я могу своей функцией идти в том направлении, в котором мне надо. Это уже другой тип итератора совсем. Ходить вперёд — частый, хотя и распространённый, случай.

                                                              Понятно, что я не буду утверждать, что вот Haskell-way — самый лучший, я просто к тому, что дьявол в деталях. В придуманных примерах важнее одно, в реальных задачах — другое, причём в каждой что-то своё.


                                                              1. ffriend
                                                                13.10.2015 14:13

                                                                Уж лучше идиоматично через list comprehensions, а то так и нечитаемее, и тормознее. А если идиоматично, то уже bar оттуда не вычленить так просто, композиционность страдает.

                                                                Ну так и язык не функциональный, понятно, что сложные функциональные штуки типа каррирования и композиции функции в нём будут хотя бы чуть-чуть, но сложнее синтаксически :D

                                                                Как же это не обладает, когда _может_ обладать (у меня там недаром f и g — произвольные функции), а это для оптимизатора весьма важно.

                                                                А, вы про `f` и `g`. Ну, тут есть 2 момента. Во-первых, Python по-умолчанию считает своих разработчиков достаточно вменяемыми, чтобы не передавать в `filter` и `map` функции с побочными эффектами. Если кто-то всё-таки передаёт, то тут уж извините, отстрелить себе ногу, как известно, можно в любой ситуации. Во-вторых, Python — это как раз тот язык, в котором про оптимизацию особо не думают. Если хотите, могу переписать всё это на высокопроизводительной Julia ;)

                                                                Можно даже бесконечные деревья (как и списки) строить. Необязательно последовательно. Те же самые filter, map, что-либо ещё.

                                                                Ну так ради б-га, кто ж мешает. Python здесь ничем не отличается от Haskell (или Java, или C++): выбираете абстрактную функцию с поведением (например, `map`), задаёте требуемый интерфейс (например, `get_next`), реализуете интерфейс для своего класса и вуа-ля!

                                                                Итератор ходит вперёд. По дереву я могу своей функцией идти в том направлении, в котором мне надо. Это уже другой тип итератора совсем. Ходить вперёд — частый, хотя и распространённый, случай.

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


                                                                1. VoidEx
                                                                  13.10.2015 14:37
                                                                  +2

                                                                  Ну так и язык не функциональный, понятно, что сложные функциональные штуки типа каррирования и композиции функции в нём будут хотя бы чуть-чуть, но сложнее синтаксически :D

                                                                  Чуть-чуть — это мягко сказано :)

                                                                  Python по-умолчанию считает своих разработчиков достаточно вменяемыми

                                                                  Да Python вообще динамический :) Так что Bondage & Discipline явно не в его стиле, конечно

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

                                                                  Речь не о том, можно или нет. В любом Тьюринг-полном можно много чего. Вопрос в том, чего это стоит.

                                                                  -- | Дерево
                                                                  data TreeT a t = Empty | Node a [t]
                                                                  type Tree a = Fix (TreeT a)
                                                                  
                                                                  -- | Бесконечное дерево, в узле n, поддеревья: genTree (n + 1) и genTree (n + 2)
                                                                  genTree n = Fix (Node n $ map genTree [n + 1, n + 2])
                                                                  
                                                                  -- | Свернём в список, первый элемент - узел, а затем идут свернутые поддеревья, которые обходятся в обратном порядке (т.е. сначала сворачивается последнее поддерево, затем предпоследнее, и т.п.)
                                                                  foo = cata f (genTree 0) where
                                                                    f Empty = []
                                                                    f (Node x xs) = x : concat (reverse xs) -- reverse - это вот обратный порядок уже обойденных поддеревьев, мы лишь склеиваем "готовые" списки
                                                                  
                                                                  -- | Берём первые 10 элементов
                                                                  test = take 10 foo -- [0,2,4,6,8,10,12,14,16,18]
                                                                  


                                                                  Вот здесь дерево ленивое — две строки.
                                                                  foo — обходит поддеревья в обратном порядке
                                                                  test — берёт только первые 10 от «бесконечного» дерева
                                                                  В foo можно было не брать библиотечную cata (катаморфизм), а обходить ручками, если хочется более полный контроль, хотя помимо cata там есть и другие вещи, к которым и так сводится множество обходов и обработок.


                                                                  1. ffriend
                                                                    14.10.2015 01:19

                                                                    Чуть-чуть — это мягко сказано :)

                                                                    Эээ, это вы с учётом того, что приведённый кусок — это готовый к исполнению код, а конкретно ваш пример:

                                                                    foo = take n
                                                                    bar = filter f
                                                                    baz = map g
                                                                    quux = foo . bar . baz
                                                                    

                                                                    транслируется в

                                                                    foo = partial(itake, n)
                                                                    bar = partial(ifilter, f)
                                                                    baz = partial(imap, g)
                                                                    quux = compose(foo, bar, baz)
                                                                    

                                                                    Так что да, я бы сказал, что именно чуть-чуть.

                                                                    Вот здесь дерево ленивое — две строки.

                                                                    Нуу, тут хоть и две строчки, но специальных фич Haskell-а целая куча — и ленивые вычисления, и рекурсия через фиксированную точку, и развитая система типов. Я даже не буду пытаться повторить это на императивном динамически-типизированном Python. Но просто, чтобы показать, что всё возможно и на нём, вот вам простая реализация рекурсивного дерева:

                                                                    class Tree(object):
                                                                        def __init__(self, n):
                                                                            self.n = n
                                                                    
                                                                        def __getattr__(self, name):
                                                                            if name == 'left':
                                                                                return Tree(self.n + 1)
                                                                            elif name == 'right':
                                                                                return Tree(self.n + 2)
                                                                            else:
                                                                                raise AttributeError("No such attribute: %s" % (name))
                                                                    
                                                                        def __repr__(self):
                                                                            return "Tree(%s)" % (self.n,)
                                                                    

                                                                    И пример использования:

                                                                    In [30]: t = Tree(1)
                                                                    
                                                                    In [31]: t
                                                                    Out[31]: Tree(1)
                                                                    
                                                                    In [32]: t.right
                                                                    Out[32]: Tree(3)
                                                                    
                                                                    In [33]: t.right.right
                                                                    Out[33]: Tree(5)
                                                                    
                                                                    In [34]: t.right.left
                                                                    Out[34]: Tree(4)
                                                                    

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


                                                                    1. VoidEx
                                                                      14.10.2015 10:18

                                                                      Нуу, тут хоть и две строчки, но специальных фич Haskell-а целая куча — и ленивые вычисления, и рекурсия через фиксированную точку, и развитая система типов.

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

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

                                                                      Согласен, приятно было пообщаться.


                            1. qw1
                              12.10.2015 23:26

                              Ну пусть будет у нас пул из 1000 буферов, пусть даже каждый буфер уже будет нужного размера — что угодно. Мы вызываем нашу функцию, результат записываем в буфер #1, затем в буфер #2, затем в буфер #3

                              Да кстати это плохое решение, если у моего процессора 24 МБ кеша L3, а я реализую быстрое возведение в степень 4-мегабайтных матриц. На Си++ всё бы влезло в кеш, в случае с тысячей буферов — уже нет.


                          1. qw1
                            12.10.2015 23:24

                            Кто мешает написать функцию ФП с «выделением» памяти, которое на самом деле будет просто возвращать ссылку на область памяти из предвыделенного пула?

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


                            1. knagaev
                              12.10.2015 23:28

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


                              1. qw1
                                13.10.2015 01:08

                                Фунция, которая вызывает не чистую функцию «получи следующий буфер», сама становится не чистой. Значит, многие оптимизации к ней уже не применимы.


            1. beeruser
              10.10.2015 22:42

              «Буфер `c` передаётся извне и модифицируется, ровно так же, как в примере с `DoSomething`»

              Он не модифицируется (для этого необходимо чтение), а _полностью перезаписывается_.
              Ваш пример чисто функциональный 'с = a + b'.
              Причём тут DoSomething() вообще?

              Нет никакой разницы между C sum(A B) и sum(*C, const *A, const *B)
              Когда вы в том же C возвращаете структуру, её адрес передаётся первым параметром.


            1. beeruser
              10.10.2015 22:51

              "«Каждый раз новый буфер» означает, что вы можете сохранять полученное значение сколь угодно долго и оно никогда никем не будет модифицированной."
              Да кто ж мешает. Сделайте «бесконечный» пул и храните там.
              «Сколь угодно долго» хранить объект на который никто не ссылается — бессмысленно.


              1. ffriend
                10.10.2015 23:41

                Он не модифицируется (для этого необходимо чтение), а _полностью перезаписывается_.
                Ваш пример чисто функциональный 'с = a + b'.

                Прочитайте описание функции внимательно — предыдущее значение `C` считывается наравне с `A` и `B`.

                Причём тут DoSomething() вообще?

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

                Нет никакой разницы между C sum(A B) и sum(*C, const *A, const *B)

                Разница в том, что в первом случае `sum()` сначала конструирует новый объект в памяти, а во втором результат сразу последовательно пишется в преаллоцировнный (и переиспользуемый) `C`. Чтобы совсем очевидно было (на псевдо-Си):

                array sum(array a, array b)
                {
                    array c = zeros(length(a));    // мы только что выделили кусок памяти
                    for (i = 0, i < ) {
                        c[i] = a[i] + b[i];
                    }
                    return c; 
                }
                


                array sum(array a, array b, array c)
                {
                    for (i = 0, i < ) {
                        c[i] = a[i] + b[i];   // заполняем преаллоцировнный буфер
                    }
                    return c; 
                }
                

                Как результат: в первом случае результат более «предсказуемый», но мы получили существенное снижение производительности; во втором переменная `c` до и после вызова функции `sum` будет иметь разное значение, но мы работаем на константном объёме памяти, даже если вызовем функцию 20000 раз (примерно столько раз в секунду вызывается функция, которую я сейчас оптимизирую).

                Да кто ж мешает. Сделайте «бесконечный» пул и храните там.

                А память чистить не будем?


  1. dtestyk
    10.10.2015 16:11

    Мне кажется, что правильное именование методов более способствует возможности продумывать код:
    >DoSomething(x)
    а почему не TurnToFalse(x) или хотя бы Process(x)?
    >cust1.Equals(cust2);
    опять же cust1.IsSameInstance(cust2), cust1.HasSameValue(cust2),

    более того cust1.HasSameTypeWith(cust2), cust1.HasSubTypeOf(cust2), cust1.HasCommonBaseTypeWith(cust2)

    >будет ли свойство Address равно null или нет
    ну это просто: cust.Address.Country == null
    но с пунктом о корректной инициализации согласен безоговорочно

    >GetHashCode
    ComputeHashCode

    >repo.GetById
    а почему не repo.TryGetById?, ведь может и не найти
    repo.GetByIdOrDefault найдет обязательно

    еще в C# есть nullable types
    msdn.microsoft.com/en-us/library/1t3y8s4s.aspx(полезный пример по ссылке)


  1. RomanPyr
    10.10.2015 16:24

  1. Vestild
    10.10.2015 17:51
    +1

    Кажется тут есть фундаментальная проблема. F# и Scala построены на .NET и Java стеках, (не только на CIL или JVM).
    Зачем? Чтобы использовать базовую библиотеку (весьма богатую в случае .net) или сторонние библиотеки (коих дофига и там и там).
    И тут вся предсказуемость летит к чертям.
    Не то, чтобы это делало такие фишки языка бесполезными, в конце концов мы чаще вызываем другие написанные нами же функции. Но надеяться на полную предсказуемость в условиях таких сложных стеков кажется бесполезно.


  1. gaelpa
    10.10.2015 19:00
    -2

    Я вот не понял на кой вначале упомянут F#? Типа это язык, лишенный обозначенных проблем? Тогда почему ни к одному примеру не идет вариант «как это прекрасно выглядит на F#»?

    Или я не понял, а Ф — тоже «плохой язык»?
    Где решение то обозначенных проблем?


    1. knagaev
      10.10.2015 19:25

      А вы внимательно читали статью? :)


  1. defuz
    11.10.2015 00:31
    +1

    Интерестно, что Rust проходит одновременно по всем пунктам.


  1. SirEdvin
    12.10.2015 00:55

    Забавно, что вопросы о том, зачем переходить с Java на Scala и Clojure (вот тут я вообще не знал, что на него переходят) не поменялись с приходом лямбд.

    Переходят из-за синтаксиса и скорости разработки, теряя в скорости работы программы.


  1. whitepen
    15.10.2015 14:49
    -1

    Тысяче-первая статья о второстепенных проблемах, которые мало кого волнуют. Теоретики потому теоретизируют, что практически ничего не пишут. Основная проблема — как сделать вот этот кусок программы не зависимым вот от этого куска программы. Допустим два куска имеют разные интерфейсы — за день можно настрочить огромную формальную обертку одного в другое. Ну и что, что избыточный и глупый код — зато он делается на раз. А давайте программировать сверху вниз, по плану, по порядку. А на деле всегда снизу вверх — уж что получится в полном беспорядке. Каждое наследование — это порождение новой сучности. Вот возьмем например клиента и товар — как хорошо, как любят такие примеры. А вот другой пример: абракадабра12 наследуется от нонсенса16, который наследуется от абстракция_палата_6. Всего то одна тысяча сучностей, нетрудно запомнить, все логично и последовательно. Порой лучше написать повторяющийся код только для того, чтобы класс не зависел от других классов. Вместо наследования влепить интерфейс и сделать отдельно. Ах как это не логично, ах как это избыточно, ах на диск не поместится.