Вероятно, хватит рекомендовать «Чистый код»

Original author: quarantine 'em
  • Translation
Возможно, мы никогда не сможем прийти к эмпирическому определению «хорошего кода» или «чистого кода». Это означает, что мнение одного человека о мнении другого человека о «чистом коде» обязательно очень субъективно. Я не могу рассматривать книгу Роберта Мартина «Чистый код» 2008 года с чужой точки зрения, только со своей.

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

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

Но в этой главе есть и более сомнительные утверждения. Мартин говорит, что аргументы логического флага — плохая практика, с чем я согласен, потому что неприкрашенные true или false в исходном коде непрозрачны и неясны по сравнению с явными IS_SUITE или IS_NOT_SUITE… но рассуждения Мартина скорее сводятся к тому, что логический аргумент означает, что функция делает больше, чем одну вещь, чего она не должна делать.

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

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

Первое правило: функции должны быть компактными. Второе правило: функции должны быть ещё компактнее. Я не могу научно обосновать своё утверждение. Не ждите от меня ссылок на исследования, доказывающие, что очень маленькие функции лучше больших. Я могу всего лишь сказать, что я почти четыре десятилетия писал функции всевозможных размеров. Мне доводилось создавать кошмарных монстров в 3000 строк. Я написал бесчисленное множество функций длиной от 100 до 300 строк. И я писал функции от 20 до 30 строк. Мой практический опыт научил меня (ценой многих проб и ошибок), что функции должны быть очень маленькими.

[...]

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

Весь этот совет завершается листингом исходного кода в конце главы 3. Этот пример кода является предпочтительным рефакторингом Мартина класса Java, происходящего из опенсорсного инструмента тестирования FitNesse.

package fitnesse.html;

import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;

public class SetupTeardownIncluder {
  private PageData pageData;
  private boolean isSuite;
  private WikiPage testPage;
  private StringBuffer newPageContent;
  private PageCrawler pageCrawler;


  public static String render(PageData pageData) throws Exception {
    return render(pageData, false);
  }

  public static String render(PageData pageData, boolean isSuite)
    throws Exception {
    return new SetupTeardownIncluder(pageData).render(isSuite);
  }

  private SetupTeardownIncluder(PageData pageData) {
    this.pageData = pageData;
    testPage = pageData.getWikiPage();
    pageCrawler = testPage.getPageCrawler();
    newPageContent = new StringBuffer();
  }

  private String render(boolean isSuite) throws Exception {
     this.isSuite = isSuite;
    if (isTestPage())
      includeSetupAndTeardownPages();
    return pageData.getHtml();
  }

  private boolean isTestPage() throws Exception {
    return pageData.hasAttribute("Test");
  }

  private void includeSetupAndTeardownPages() throws Exception {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
  }


  private void includeSetupPages() throws Exception {
    if (isSuite)
      includeSuiteSetupPage();
    includeSetupPage();
  }

  private void includeSuiteSetupPage() throws Exception {
    include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
  }

  private void includeSetupPage() throws Exception {
    include("SetUp", "-setup");
  }

  private void includePageContent() throws Exception {
    newPageContent.append(pageData.getContent());
  }

  private void includeTeardownPages() throws Exception {
    includeTeardownPage();
    if (isSuite)
      includeSuiteTeardownPage();
  }

  private void includeTeardownPage() throws Exception {
    include("TearDown", "-teardown");
  }

  private void includeSuiteTeardownPage() throws Exception {
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
  }

  private void updatePageContent() throws Exception {
    pageData.setContent(newPageContent.toString());
  }

  private void include(String pageName, String arg) throws Exception {
    WikiPage inheritedPage = findInheritedPage(pageName);
    if (inheritedPage != null) {
      String pagePathName = getPathNameForPage(inheritedPage);
      buildIncludeDirective(pagePathName, arg);
    }
  }

  private WikiPage findInheritedPage(String pageName) throws Exception {
    return PageCrawlerImpl.getInheritedPage(pageName, testPage);
  }

  private String getPathNameForPage(WikiPage page) throws Exception {
    WikiPagePath pagePath = pageCrawler.getFullPath(page);
    return PathParser.render(pagePath);
  }

  private void buildIncludeDirective(String pagePathName, String arg) {
    newPageContent
      .append("\n!include ")
      .append(arg)
      .append(" .")
      .append(pagePathName)
      .append("\n");
  }
}

Повторю ещё раз: это собственный код Мартина, написанный по его личным стандартам. Это идеал, представленный нам в качестве учебного примера.

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

Давайте проигнорируем import с подстановочными знаками.

У нас есть два публичных статических метода, один приватный конструктор и пятнадцать приватных методов. Из пятнадцати приватных методов аж тринадцать либо имеют побочные эффекты (они изменяют переменные, которые не были переданы им в качестве аргументов, например buildIncludeDirective, который имеет побочные эффекты на newPageContent), либо вызывают другие методы, которые имеют побочные эффекты (например, include, который вызывает buildIncludeDirective). Только isTestPage и findInheritedPage выглядят как будто без побочных эффектов. Они по-прежнему используют переменные, которые в них не передаются (pageData и testPage соответственно), но, кажется, делают это без побочных эффектов.

На этом этапе вы можете сделать вывод, что, возможно, определение Мартина «побочный эффект» не включает переменные-члены объекта, метод которого мы только что вызвали. Если мы примем это определение, то пять таких переменных, pageData, isSuite, testPage, newPageContent и pageCrawler, неявно передаются каждому вызову приватного метода, и это считается нормальным; любой приватный метод свободен делать всё, что ему нравится, с любой из этих переменных.

Но это неправильное предположение! Вот собственное определение Мартина из более ранней части этой главы:

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

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

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

private String render(boolean isSuite) throws Exception {
   this.isSuite = isSuite;
  if (isTestPage())
    includeSetupAndTeardownPages();
  return pageData.getHtml();
}

Почему этот метод имеет побочный эффект установки значения this.isSuite? Почему бы просто не передать isSuite как логическое значение более поздним вызовам метода? Почему мы возвращаем pageData.getHtml() после того, как потратили три строки кода, ничего не сделав с pageData? Мы могли бы сделать обоснованное предположение, что includeSetupAndTeardownPages имеет побочные эффекты на pageData, но тогда что? Мы не можем знать ни того, ни другого, пока не посмотрим. И какие ещё побочные эффекты это оказывает на другие переменные-члены? Неопределённость так возрастает, что мы внезапно задаёмся вопросом, может ли isTestPage тоже иметь побочные эффекты. (А что это за выступ? А где фигурные скобки?)

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

private WikiPage findInheritedPage(String pageName) throws Exception {
  return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}

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

Во-вторых, содержимое pageData уничтожается. В отличие от переменных-членов (isSuite, testPage, newPageContent и pageCrawler), мы на самом деле не владеем pageData для изменения. Он первоначально передаётся в общедоступные методы визуализации верхнего уровня внешним вызывающим объектом. Метод рендеринга выполняет большую работу и в конечном итоге возвращает строку HTML. Однако во время этой работы в качестве побочного эффекта pageData деструктивно модифицируется (см. updatePageContent). Конечно, было бы предпочтительнее создать совершенно новый объект PageData с нашими желаемыми модификациями и оставить оригинал нетронутым? Если вызывающий объект попытается использовать pageData для чего-то другого, он может быть очень удивлён тем, что произошло с его содержимым. Но именно так вёл себя исходный код до рефакторинга Мартина. Он сохранил это поведение, хотя и закопал его очень эффективно.

*
Неужели вся книга такая?

В основном, да. «Чистый код» смешивает обезоруживающую комбинацию сильных, вечных советов и советов, которые очень сомнительны или устарели. Книга фокусируется почти исключительно на объектно-ориентированном коде и призывает к достоинствам SOLID, исключая другие парадигмы программирования. Он фокусируется на коде Java, исключая другие языки программирования, даже другие объектно-ориентированные языки. Есть глава «Запахи и эвристические правила», которая представляет собой не более чем список довольно разумных признаков, которые следует искать в коде. Но есть несколько в основном пустословных глав, где внимание сосредоточено на трудоёмких отработанных примерах рефакторинга Java-кода. Есть целая глава, изучающая внутренние компоненты JUnit (книга написана в 2008 году, так что вы можете себе представить, насколько это актуально сейчас). Общее использование Java в книге очень устарело. Такого рода вещи неизбежны — книги по программированию традиционно быстро устаревают — но даже для того времени предоставленный код плох.

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

@Test
  public void turnOnLoTempAlarmAtThreashold() throws Exception {
    hw.setTemp(WAY_TOO_COLD);
    controller.tic();
    assertTrue(hw.heaterState());
    assertTrue(hw.blowerState());
    assertFalse(hw.coolerState());
    assertFalse(hw.hiTempAlarm());
    assertTrue(hw.loTempAlarm());
  }

и гордо переделывает его:

@Test
  public void turnOnLoTempAlarmAtThreshold() throws Exception {
    wayTooCold();
    assertEquals(“HBchL”, hw.getState());
  }

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

*
Автор представляет три закона TDD:

Первый закон. Не пишите код продукта, пока не напишете отказной модульный тест.

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

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

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

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

*
Есть глава «Объекты и структуры данных», где автор приводит такой пример структуры данных:

public class Point {
  public double x;
  public double y;
}

и такой пример объекта (ну, интерфейс для одного объекта):

public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}

Он пишет:

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

И… это всё?

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

*
Я не собираюсь переписывать все остальные мои заметки. У меня их слишком много, и перечислять всё, что я считаю неправильным в этой книге, заняло бы слишком много времени. Я остановлюсь на ещё одном вопиющем примере кода. Это генератор простых чисел из главы 8:

package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
  private static int[] primes;
  private static ArrayList<Integer> multiplesOfPrimeFactors;

  protected static int[] generate(int n) {
    primes = new int[n];
    multiplesOfPrimeFactors = new ArrayList<Integer>();
    set2AsFirstPrime();
    checkOddNumbersForSubsequentPrimes();
    return primes;
  }

  private static void set2AsFirstPrime() {
    primes[0] = 2;
    multiplesOfPrimeFactors.add(2);
  }

  private static void checkOddNumbersForSubsequentPrimes() {
    int primeIndex = 1;
    for (int candidate = 3;
         primeIndex < primes.length;
         candidate += 2) {
      if (isPrime(candidate))
        primes[primeIndex++] = candidate;
    }
  }

  private static boolean isPrime(int candidate) {
    if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
      multiplesOfPrimeFactors.add(candidate);
      return false;
    }
    return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
  }

  private static boolean
  isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
    int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
    int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
    return candidate == leastRelevantMultiple;
  }

  private static boolean
  isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
    for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
      if (isMultipleOfNthPrimeFactor(candidate, n))
        return false;
    }
    return true;
  }

  private static boolean
  isMultipleOfNthPrimeFactor(int candidate, int n) {
   return
     candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
  }

  private static int
  smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
    int multiple = multiplesOfPrimeFactors.get(n);
    while (multiple < candidate)
      multiple += 2 * primes[n];
    multiplesOfPrimeFactors.set(n, multiple);
    return multiple;
  }
}

Что это за код? Каковы названия методов? set2AsFirstPrime? smallestOddNthMultipleNotLessThanCandidate? Это должен быть чистый код? Ясный, интеллектуальный способ просеивания простых чисел?

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

*
Я написал это эссе, потому что постоянно вижу, как люди рекомендуют «Чистый код». Я почувствовал необходимость предложить антирекомендацию.

Первоначально я читал «Чистый код» в группе на работе. Мы читали по главе в неделю в течение тринадцати недель.

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

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

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

См. также:

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 427

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

    Как для 2008 года, я бы предложил «Совершенный код» (Code Complete) Макконнелла для начинающих разработчиков — гораздо менее категоричная книга, однако в 2020 году не все главы уже актуальны.
      +4
      Тоже поддержу «Совершенный код».
        +11

        +1 за Макконела. Разносторонне, глубоко и без лишнего эпатажа в стиле «я писал код еще на древних египетских скрижалях»

          +5
          какие, например, неактуальны?
            +4
            К сожалению, в данный момент книга не под рукой, не помню всех деталей. Насколько помню, из устаревших моментов — краткий обзор языков программирования, работа с форматированием и учет скобочек (то, что в наше время делает любая IDE), некоторые моменты недостаточно строги (например, Макконнелл рекомендует не более 7 аргументов для функции, что по современным меркам довольно много) и некоторые другие мелочи проскальзывают.
            Но, если вспомнить, что эта книга была, пожалуй, одной из первых, дававших базовые знания о грамотной промышленной разработке, много сейчас ей можно простить.
            По сути, многие вещи, которые в ней были собраны вместе, сейчас являются стандартом и must have для всех профессиональных разработчиков — поэтому ее по-прежнему стоит рекомендовать.
            +1
            Тоже за «Совершенный код» Стива Макконнелла. Прочитал её в 2011 будучи junior developer'ом и она сделала меня как программиста. Сейчас далеко не во всем согласен с автором. Но до сих пор рекомендую.
              0
              Шёл под заголовок статьи, чтобы убедиться, что Макконелла вспомнили в комментариях.
              Время от времени перелистываю, прям чтобы неактуального не вижу, кое-что требует уточнения, пожалуй, но не так, чтоб аж отмены/переработки.
              +20
              Я согласен, что плохо для функции вносить неожиданные изменения в переменные своего собственного класса.

              Методы в ООП взаимодействуют с состоянием объекта. Когда методы перестают это делать, а состояние начинает проталкиваться через аргументы, то код превращается в обычное процедурное программирование. Разве не так?
                +1
                В примере речь не про состояние, а про параметр с которым нужно выполнить операции. Такие параметры лучше передавать аргументами метода.
                С более современными подходами, можно и состояние передавать через аргумент. Поля в таком случае скорее нужны для связей с другими объектами. И это по прежнему может быть ООП.
                  0
                  И это по прежнему может быть ООП.
                  — но уже не Чистый Код.
                    +1
                    Такие параметры лучше передавать аргументами метода

                    Вообще, идея интересная. Но ИМХО не для джавы и прочих ЯП, которые выбирают с учетом скорости разработки. Это что-то вроде const-correctness в c++, где вы можете помечать метод как const, гарантируя, что этот метод не может изменять поля своего объекта. Вот тут как раз люди с подобным осознанно заморачиваются (и все бы так делали). Только вот в плюсах это контракт типа «всё или ничего» — отдельные поля для указания выбрать нельзя. Ну есть еще mutable-поля, что по сути дает возможность их менять даже в конст-методах, но опять же, во всех. Если передавать все нужно аргументами метода — тут вылезут другие недостатки, вроде лишнего копирования тонны аргументов на каждый вызов и прочее, что тоже, согласитесь, не лучший вариант. Проскользнула мысль — разрешать для изменения указанные поля через синтаксис типа аттрибутов в C#, но многословность тоже никуда не исчезнет… Вообще, лично я бы смирился с тем, что в классе его поля — это как единая контролируемая неделимая сущность, и разрешить их изменять без договоренности «по одному» — нет большой выгоды. Но вот насчет статических полей и всяких глобальных переменных (если таковые имеют место быть в том или ином ЯП) — для их изменения как раз не помешало бы вводить те самые разрешения «по одному», ибо сайд-эффекты как раз имхо чаще завязаны на них (всякие errno и прочие). Плюс такого подхода — количество «разрешений» будет гораздо меньше, чем в первом варианте, а так же будет возникать ситуация, когда функции, изменяющие глобальные переменные, помимо своих собственных «разрешений» тащат за собой все разрешения всех вызываемых функций с сайд-эффектами. То есть, мы наглядно будем видеть, что там подкапотно ворочают в недрах вызовов. И да, этот список будет разрастаться, что будет являться показателем «я явно трогаю слишком много всего, надо что-то рефакторить», приводящее к будущим советам от новых гуру типа «не более пары разрешений на функцию» или «список разрешений должен помещаться в один экран» :)
                    Как-то так.
                    +7
                    Да, это так. Но тут скорее речь о том, что если есть выбор между тем, менять состояние объекта или не менять, то лучше избегать побочных эффектов, то есть не менять.
                    Например:
                    auto image = getImage();
                    image.mirror(); // плохой метод, меняет состояние объекта
                    auto mirrorImage = image.mirrored(); // хороший метод, состояние не модифицируется, но есть копирование
                    

                    Тем более, что в современных С++ копирования можно избежать, если добавить перегрузку от rvalue-ref (метод Image mirrored() &&), например:
                    auto mirrorImage = getImage().mirrored(); // отлично, копирования нет, внешних побочных эффектов нет
                    

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

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

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

                      В программировании контроллеров — тоже.

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


                        Нет, не считаю, я считаю что не надо заниматься premature optimization и кидаться сразу делать АПИ мутабельным просто потому что «это быстрее».
                        Всегда можно воспользоваться вторым вариантом с перегрузкой по rvalue. Можно даже пойти дальше и оставить только эту перегрузку и не перегружать метод от lvalue, тогда уже компилятор будет бить по рукам, а не профайлер. На эту тему был доклад на cpp russia в прошлом году. Возможно, если бы писал класс Image, я бы так и сделал=)
                        Или, раз уж мы затронули тему игровых движков, то можно вспомнить статью Кармака 8 летней давности где он рассуждает о том что pure functions это хорошо, а сайд эффекты — плохо.
                        Это ложная дихотомия — либо мутабельность, либо скорость — можно взять и то и то, было бы желание.
                        Возможно, пример с картинкой не самый удачный, просто первое, что пришло в голову.
                        Вероятно, какой-нибудь class Matrix и transpose() vs. transposed() было бы лучшим примером.
                          0
                          Упс, допустил опечатку, должно быть
                          либо иммутабельность, либо скорость
                            0
                            Это ложная дихотомия — либо мутабельность, либо скорость — можно взять и то и то, было бы желание.

                            Не всегда. Как говорит и Кармак в вашей статье в разделе Performance Implications.
                            +4
                            ну смотрите, у вас есть копирование. Причем картинки, тяжеловесного объекта. Вы всерьез считаете, что это всегда хорошо?

                            Так ведь никто и не заставляет сразу же делать копирование. Для широкого класса операций (в том числе на картинках) можно результатом mirrored() вернуть нечnо, что ведёт себя как отзеркаленная картинка, но на деле просто осуществляет трансляцию координат из оригинального изображения при доступе. А например image.mirrored().mirrored() вообще вернёт image. Да даже с рисованием поверх этой картинки можно такие фокусы проворачивать, если операция рисования на самом деле создаёт только слой поверх оригинального изображения, а основной массив пикселей остаётся лежать как был.
                            Более того — в первой редакции этого кода можно и пожрать память, получить уже какой-то рабочий код, который умеет что-то делать с картинками, а потом начинать без изменения API его оптимизировать введением лени, отображений, трансляторов, определять когда нужно спекать эти отображения вместе, а когда не стоит, и прочую "магию".

                              0

                              Причем мы в таком случае бесплатно полчаем всякие undo/redo и прочие механизмы, потому что не ломаем данные которые у нас были.

                                0

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

                                  0

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

                                    0

                                    Я говорил чуть про другое — когда из десяти слоёв undo при добавлении ещё одного слоя получается снова десять (или меньше), просто в каком-нибудь слое N будет лежат результат объединения слоёв N и N-m. Именно такая операция будет деструктивна к undo, зато позволит сэкономить на вычислениях.

                                      0

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


                                      Мне кажется, оно так везде работает

                                    0
                                    Это зависит от того, требуется ли Undo пользователю, если да то можно при числе слоёв больше трёх хранить в последнем слое снимок наложения всех предыдущих.
                              +1
                              Если в реализации метода добавить проверку идентичности, то можно обойтись одним методом:
                              auto image = getImage();
                              image.mirrorTo(image);
                              
                              или
                              auto image = getImage();
                              auto mirrorImage = newImage()
                              image.mirrorTo(mirrorImage);
                              
                                0

                                Неявная работа функции + лишнее действие. Не очень удобно.

                                  +3

                                  Такой подход хорош только для супер-низкоуровневых АПИ где мы таким образом контролируем выделение памяти.


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

                                  0
                                  Так проблема в том, что если в апи Image::getMirrored, то как ни расставляй референсы, а все равно в результате получишь две картинки: исходную и перевёрнутую. Может быть, что мутирующая операция отражения очень дешевая (просто преключает флаг внутри), а операция копирования не такая дешевая и ресурсов не дофига. И вот в такой ситуации, оптимизация с заменой апи на мутирующий Image::mirror() может оказаться очень дорогой с точки зрения разработки, придётся переписывать вообще всё. Так что лучше сразу стараться дизайнить оптимально.
                                    +2
                                    В случае move-only API «старый» объект будет содержать пустую картинку, а clang-tidy будет предупреждать, если вы захотите этим объектом воспользоваться кроме как попытавшись записать новый:
                                    auto image = getImage();
                                    auto mirrored = std::move(image).mirrored();
                                    std::cout << image.size() << std::endl; // warning, bugprone-use-after-move
                                    image = getAnotherImage(); // OK
                                    

                                    Такой подход лучше тем что существует тулинг, который позволяет отслеживать неправильное использование «мувнутых» объектов, а для общего случая (например mirror/nonmirror) такого тулинга нет — только программист знает, что ему нужно.
                                    Если вам нужна копия, то придется явно это написать, и ревьюверу будет видно что тут тяжелая копия:
                                    auto image = getImage();
                                    auto mirrored = Image(image).mirrored();
                                    


                                    Или можно воспользоваться Copy-On-Write (если объект полностью иммутабельный, то вам даже deep copy не нужно делать on write, что упрощает код и устраняет большинство проблем COW) и применить ваше решение с флажком «orientation» — тогда и копирование дешевое и иммутабельность сохраняется.
                                      0
                                      Если вам нужна копия, то придется явно это написать


                                      Проблема в том, что вы же не думали об оптимизации заранее, сервис был маленький, ресурсов вагон и программист из комментария выше уже написал такой код:
                                      auto image = getImage();
                                      auto mirrorImage = image.mirrored(); // хороший метод, состояние не модифицируется, но есть копирование
                                      setImage(mirrorImage);
                                      

                                      В этом случае ему, на самом деле, копия была не нужна, исходную картинку можно было выбросить. В другом случае он же написал похожий код, вроде такого:
                                      auto image = getImage();
                                      auto flippedImage = image.flip(); // хороший метод, состояние не модифицируется, но есть копирование
                                      setAnotherImage(flippedImage);
                                      

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

                                      Эта история повторилась еще много-много раз. И когда вдруг поняли, что надо бы пооптимизировать лишние копирования, придется каждый такой кейс изучать заново и смотреть — где копирование было необходимым, а где можно и move. Считай, всю работу с картинками придется переписать.
                                        +2
                                        В чем проблема то? Есть кейс где подходит копирование, есть кейс где подходит мутирующий метод. Это С++, тут думать надо на каждом шагу. Решения чтобы можно было не думать, и при этом результат получился оптимизированный не существует.

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

                                          Проблема в том, что вы не прочитали ветку, на которую я отвечал. А именно, пропустили заявление хабраюзера о том, что в современном С++ перемещение (почти всегда) заменяет мутирующие методы.

                                            +2
                                            Я прочитал. Просто этот хабраюзер какуюто совсем херню написал.
                                              0
                                              Это да. Или к моему комменту тоже вопросы есть?
                                        0
                                        Поддержу только тезис о том, что копии лучше делать явно.

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

                                        В конечном итоге тут все зависит от реализации getImage(), в плюсах красивым образом написать ее вообще невозможно. Писать мувы в таких местах это вообще боль. Под него еще надо сам image правильно написать. Вы же понимаете что мув тоже копирует объект, просто правильным образом разруливает ссылки на тяжелые объекты внутри?

                                        Если вы так гоняете этот image, куда проще в shared_ptr его обернуть, это еще и быстрее будет.

                                        Два метода, один под явное копирование, а второй для мутации объекта будет лучшим выбором.

                                        Этот код вообще бессмысленный
                                        auto image = getImage();
                                        auto mirrored = std::move(image).mirrored();
                                        std::cout << image.size() << std::endl; // warning, bugprone-use-after-move
                                        image = getAnotherImage(); // OK

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

                                        Так куда понятнее и проще, и не нужен тулинг отслеживать мувнутые объекты
                                        auto image = getImage();
                                        image.flip();
                                        
                                        auto copyImage = Image(getImage());
                                        copyImage.flip();


                                        Если вы пишете низкоуровневый код, отталкивайтесь от производительности, а не от советов из книжек про визуально красивый код на джаве
                                          +1
                                          Этот код вообще бессмысленный


                                          Этот код иллюстрирует пример когда у вас был объект, вы его мутировали, а потом через вереницу ифов использовали:
                                          auto image = getImage();
                                          image.flip();
                                          if (someLongCondition1)
                                             foo();
                                          if (someLongCondition2)
                                             bar();
                                          // your code goes here
                                          baz(image);  // упс, вам тут нужен был исходный имадж, а не флипнутый
                                          

                                          Да, пример тривиальный (на то он и пример). В случае мувнутого объекта вам тулы скажут что вы делаете что-то не то, а в этом случае вам надо полагаться на ревью/тесты.
                                          Или вам никогда не приходилось часами отлаживать баги где десяток стейтов накладываются друг на друга?
                                            0
                                            Когда пишете код, нужно понимать что вы делаете, тогда проблем которые вы описываете будет сильно меньше.

                                            А вы предлагаете бред. Мувы будто сами багов не могут создать.
                                      0

                                      Не думаю, что класс Image должен заботиться обо всех возможных трансформациях. Сегодня надо отражать по вертикали, завтра понадобится отражать по горизонтали, ресайзить, делать Ч/Б, делать негатив, менять гамму, накладывать маску, делать блюр, и ещё сотни эффектов — всё пихать в Image?


                                      Лучше уж сделать composer, которому на входе скармливается иммутабельный Image (тот просто отдаёт владение своим битмапом, чтобы избежать лишнего копирования), потом у композера запрашиваются визуальные эффекты (которые стараются делать преобразования по месту, и могут быть вообще "ленивыми"), и на выходе создаётся результирующий иммутабельный Image (который опять же просто получает владение на получившийся битмап):


                                      auto sourceImage = getImage();
                                      auto composer = Composer{ };
                                      auto resultImage = composer
                                          .addImage(sourceImage.release())
                                          .mirrorX() // в идеале, эффекты в композиции должны быть "ленивыми",
                                          .mirrorX() // и эта пара mirrorX() должна аннигилировать при вызове compose()
                                          .mirrorY()
                                          .grayscale() // в идеале, должен выполниться первым для оптимизации последующих
                                          .negateColors()
                                          .mask(maskingImage)
                                          .gaussianBlur(10)
                                          .compose();
                                        +1

                                        А лучше даже обозвать его не Composer, а Composition, и сделать его комбинируемым с собой.

                                          0
                                          Сегодня надо отражать по вертикали, завтра понадобится отражать по горизонтали, ресайзить, делать Ч/Б, делать негатив, менять гамму, накладывать маску, делать блюр, и ещё сотни эффектов — всё пихать в Image? Лучше уж сделать composer, которому на входе скармливается иммутабельный


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

                                            Где противоречие? Можно сделать библиотеку с composer.

                                            0
                                            Не очень понятно как это решает заявленную проблему огромного количества методов — не всё ли равно, в каком классе они находятся? Но подход вполне имеет право на жизнь, что лишний раз подтверждает мое утверждение что без мутабельности можно прожить (хотя небось уже никто не помнит о чем изначально был спор). Спасибо за хороший пример, я что-то сразу не подумал о нём.
                                            Ещё, как написали ниже, можно сделать библиотеку/неймспейс с нужными функциями и сделать их stateless/pure — на вход объект картинки и на выход объект картинки. Но тут вкусовщина, кому-то нравится писать Composer(getImage())).foo().bar().baz().toImage(), кому-то foo(bar(baz(getImage()))).
                                              0

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

                                          0
                                          У меня есть подозрение, что здесь дядюшку Боба просто поняли неправильно.
                                          В достаточно старых языках программирования (FORTRAN, PL/I и большинство их ровесников) было четкое, на уровне языка, разделение вызваемых модулей на функции — которые возвращают значение на основе переданных аргументов (возможно, производя на эти аргументы какие-то побочные эффекты), и процедуры — которые что-то делают с переданными им аргументами, но значения не возвращают, и сам смысл которых — как раз в том, что для функций называлось бы побочным эффектом.
                                          И вот мне кажется, что автор «Чистого кода» использовал слово «функция» именно в этом контексте.
                                            0

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

                                            +3

                                            Нет, инкапсуляцию никто не отменял. Есть хороший доклад на тему ФП/ООП/Процедурщины, он довольно неплохо объясняет разницу между ними:


                                            https://www.destroyallsoftware.com/talks/boundaries


                                            Код кстати не какой-то хаскель/скала/идрис, а вполне приземлённый руби.


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

                                              +3

                                              А это плохо? Разве ООП нужен только ради ООП?


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

                                                +1
                                                Метод в ООП получает объект как еще один параметр. Поэтому в плане количества и явности зависимостей, между методами и функциями принципиальной разницы нет.

                                                По идее, чем больше параметров, тем сложнее. ООП стремится переложить большую часть сложности на этап создания объекта, и тем самым упростить интерфейс для конечных потребителей, уменьшая количество аргументов у метода до минимума. ФП, наоборот, оставляет потребителя разбираться с полным набором параметров самостоятельно. И в том и в другом подходе можно найти как свои преимущества, так и недостатки. Вот только практика показывает, что при попытке бездумно комбинировать разные подходы, вместо профита, получаются одни проблемы. Мода на ФП в ООП языках привела к тому, что порой на код без слез смотреть невозможно.
                                                  +1

                                                  Ну чем вот отличается инстансный метод который неявно получает this от (MonadReader MyContext m) который получает тот же самый this точно также из эмбиент контекста? На мой взгляд, совершенно ничем

                                                    0
                                                    В каком-то смысле они похожи. Ну как телевизор и картина маслом — и на то и на другое можно смотреть, даже не включая в розетку.
                                                      +4

                                                      Плохая аналогия подобна котёнку с дверцей

                                                    +2

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

                                                      0
                                                      Я может быть что-то не до конца понимаю, но что вам мешает в такой ситуации использовать интерфейсы и таким образом «указывать какие части вам интересны»?
                                                        +2

                                                        Ну вот у меня есть объект


                                                        public class SetupTeardownIncluder {
                                                          private PageData pageData;
                                                          private boolean isSuite;
                                                          private WikiPage testPage;
                                                          private StringBuffer newPageContent;
                                                          private PageCrawler pageCrawler;
                                                        ...
                                                        };

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

                                                          0
                                                          Ок, это действительно не то о чём думал я. А зачем вам вообще знать какими приватными полями пользуется какой-то метод? И зачем вам вообще знать какие приватные поля существуют у какого-то класса, который вы только используете?

                                                            0

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

                                                              0
                                                              Если вы этот код дописываете, то вы по идее видите что делает каждый метод. Или как вы его собираетесь дописывать?

                                                              А если вы просто «потребляете» чей-то там класс или библиотеку, хотите понять почему-то там что-то не работает и у вас нет исходников, то да это проблема. Но как бы access modifiers именно для того и придумали чтобы вы не видели того, что вам не хотят показывать.
                                                                +1
                                                                Если вы этот код дописываете, то вы по идее видите что делает каждый метод. Или как вы его собираетесь дописывать?

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


                                                                Но как бы access modifiers именно для того и придумали чтобы вы не видели того, что вам не хотят показывать.

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

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

                                                                  это тоже можно отнести к «вопросам к вашей IDE». И я бы даже сказал что это и нужно к ним относить. То есть для меня это всё из категории «найди все места где эта функция/переменная используется» или «найди все реализации этого интерфейса».
                                                                    +3

                                                                    Перефразирую: как не читая тела функции понять, что она может делать, а что — нет?


                                                                    В классическом ООП ответ: никак, вот есть у тебя void Foo() и можно гадать до посинения что он там делает.


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


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

                                                                      0
                                                                      А в каком-то варианте «не классического ООП» можно не читая тела функции полностью понять что функция может делать, а что не может?
                                                                        +2

                                                                        Если у меня есть функция вида (MonadReader r m, Has SomeConfig r, Has SomeParams r) => ..., то я могу не читать ни тело этой функции, ни тела всех вызываемых ей функций, чтобы понять (при некоторых очень простых и разумных дополнительных предположениях), что эта функция имеет доступ только к SomeConfig и SomeParams из окружения, и только «на чтение».

                                                                          +2

                                                                          Есть такой термин — параметричность. Очень полезная штука. Например, возьмем функцию с такой сигнатурой:


                                                                          foo : a -> a

                                                                          ну или если вам ML не нравится возьмем раст:


                                                                          fn foo<T>(t: T) -> T { ... }

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


                                                                          Как бы вы не попытались реализовать эту функцию, вы можете её реализовать только таким образом что я сказал. Если только не попытаетесь очень очень сильно саботировать сигнатуру, но обычно разработчик старается решить задачу, а не сделать аналог
                                                                          #define TRUE FALSE




                                                                          Причём в достаточно прошаренном языке (который не разрешает просто так эксепшны бросать тут и там) вам даже IDE самостоятельно сможет сгенерировать эту самую единственную реализацию:


                                                                          img


                                                                          В реальности, конечно, часто выбор больше, но все равно множество разумных реализаций (исходя из названия функции, её аргументов и результата) очень и очень невелико, а часто состоит из всего 1 варианта

                                                                            +2
                                                                            В реальности, конечно, часто выбор больше, но все равно множество разумных реализаций (исходя из названия функции, её аргументов и результата) очень и очень невелико, а часто состоит из всего 1 варианта
                                                                            Не с вами ли вы обсуждали, совсем недавно, пример, где это было нифига не так?

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

                                                                            Сложность должна где-то жить, это, увы банальная истина.

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

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


                                                                              Например, если бы я увидел вызов пары функций в монаде State мне бы в голову не пришло поменять их местами, не проверив, что ничего не поломалось. А в шарпе я в зимой такую ошибку совершил. Хотя там понятно почему так получилось: я проверил всего лишь два десятка функций на 3-4 уровня по коллстеку, а нужно было заглянуть на 8 функций внутрь чтобы увидеть что там стейт мутируется.


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

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

                                                                              0
                                                                              Из этой информации и имени функции можно понимать что она делает, не читая её тело.

                                                                              Но я правильно понимаю что такая функция «имеет доступ» исключительно к своим параметрам и всё? И скажем «внутри» у неё в принципе не может быть скажем доступа к какой-то базе данных или стороннему сервису? Или в ней тоже как-то описывается что она с ними делает и это можно понять не читая тело самой функции?
                                                                                0

                                                                                Правильно, в ней не может быть никаких хождений в БД, запросов по сети, доступа к какому-либо неявному стейту и так далее.


                                                                                если оно в фунцкии нужно то оно выглядит примерно как


                                                                                foo : (MonadHttp m, SqlBackend m) => UserId -> m User
                                                                                foo = ...
                                                                                  0
                                                                                  Ну вот теперь у вас есть «явный стейт». Вы можете мне сказать что вот эта ваша функция foo делает с SqlBackend не читая тело самой функции?
                                                                                    0

                                                                                    Ну если мне нужна такая детализация, я могу дальше уочнить, например SqlReadonlyBackend m или UserRepository m, ну и так далее.


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

                                                                                      0
                                                                                      Ну если мне нужна такая детализация

                                                                                      Дело не в детализации, а в том что там конкретно делается. Вы знаете что конкретно ваша фунцкия запишет в UserRepository не читая её тело?

                                                                                      я могу дальше уочнить, например SqlReadonlyBackend m или UserRepository m, ну и так далее.

                                                                                      Вот именно «я могу». А кто-то может так и не делать. Ну то есть получите вы библиотеку от человека, который «не смог» и вместо UserRepository запихал в параметры весь SqlBackend. И дальше что?

                                                                                      То есть вот эти ваши «я могу» это уже на мой взгляд начинаются code conventions. А их и в ООП никто не отменял.
                                                                                        –1
                                                                                        Дело не в детализации, а в том что там конкретно делается. Вы знаете что конкретно ваша фунцкия запишет в UserRepository не читая её тело?

                                                                                        Знаю, одно из нескольких действий которые есть в репозитории.


                                                                                        Вот именно «я могу». А кто-то может так и не делать. Ну то есть получите вы библиотеку от человека, который «не смог» и вместо UserRepository запихал в параметры весь SqlBackend. И дальше что?

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


                                                                                        То есть вот эти ваши «я могу» это уже на мой взгляд начинаются code conventions. А их и в ООП никто не отменял.

                                                                                        Как в мейнстрим ООП языках отличить функцию которая ходит в БД от функции которая этого не делает?

                                                                                          0
                                                                                          Знаю, одно из нескольких действий которые есть в репозитории.

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

                                                                                          Как в мейнстрим ООП языках отличить функцию которая ходит в БД от функции которая этого не делает?

                                                                                          А как в вашем примере отличить в какой репозиторй пишет функция получающая как параметер SqlBackend и пишет она туда или только читает?

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

                                                                                            Ну если написано так:


                                                                                            foo : (UserRepository m) => 
                                                                                              (str : String) -> m (WriteResult m str)

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


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

                                                                                            Ну если я увижу функцию


                                                                                            addUser : (UserRepository m) -> UserName -> UserPassword -> m ()

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

                                                                                              +1
                                                                                              Ну если написано так:

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

                                                                                              Ну если я увижу функцию.

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


                                                                                              Ну так и если я увижу функцию
                                                                                              UserRepository.AddUser(userName, userPassword)

                                                                                              то предположу тоже самое. И чем мои предположения хуже ваших? :)
                                                                                                0
                                                                                                Я честно говоря вот так на первый взгляд не могу со 100% уверенностью понять что функция WriteResult только пишет и больше ничего не делает. То есть может я синтаксис не особо хорошо понимаю, но из чего это должно следовать?

                                                                                                из связи результата и входного параметра.


                                                                                                то предположу тоже самое. И чем мои предположения хуже ваших? :)

                                                                                                Да нет, просто я вот например не вижу:


                                                                                                1. есть какое-то логгирование в этом AddUser или нет?
                                                                                                2. она может завершиться с ошибкой или нет?
                                                                                                3. а она чистая или нет, мы меняем какой-то стейт самого UserRepository (может, кэши какие-то)?
                                                                                                4. ...

                                                                                                То есть тут вопрос в том, какие предположения мы достоверно можем отмести

                                                                                                  +1
                                                                                                  из связи результата и входного параметра.

                                                                                                  А как будет выглядеть эта связь если WriteResult ещё что-то делает кроме как писать? Как мне понять что произойдёт если result по каким-то там причинам не сможет быть записан в бд? Например если он в неправильном формате?

                                                                                                  есть какое-то логгирование в этом AddUser или нет?

                                                                                                  а она чистая или нет, мы меняем какой-то стейт самого UserRepository (может, кэши какие-то)?

                                                                                                  Пониемаете, я вот лично в 99,999% cлучаев даже не хочу это знать. И по вашему получатся что ради того самого 0,001% я должен каждый раз передавать мой логгер/кэш в параметрах. Мне лично это менее удобно и создаёт больше проблем чем решает.

                                                                                                  она может завершиться с ошибкой или нет?

                                                                                                  Это вообще к ООП отношения не имеет. В той же Java есть вот такое

                                                                                                   public void init() throws CustoмException
                                                                                                  {
                                                                                                  }
                                                                                                    0
                                                                                                    А как будет выглядеть эта связь если WriteResult ещё что-то делает кроме как писать? Как мне понять что произойдёт если result по каким-то там причинам не сможет быть записан в бд? Например если он в неправильном формате?

                                                                                                    Если в сигнатуре этого нет, значит запись не может завершиться неуспешно. А учитывая, что в реальности БД всегда может поломаться, можно сделать вывод что это сигнатура описывает in-memory базу :) И Этот вывод мы смогли сделать просто из сигнатуры, ну потому что не бывает физической БД которая никогда не падает при записи.


                                                                                                    Пониемаете, я вот лично в 99,999% cлучаев даже не хочу это знать. И по вашему получатся что ради того самого 0,001% я должен каждый раз передавать мой логгер/кэш в параметрах. Мне лично это менее удобно и создаёт больше проблем чем решает.

                                                                                                    А у меня получается, что очень часто это нужно знать. А то был у меня например случай, когда я безобидную функцию вида int x = Sqr(otherInt) написал в цикле, а у меня упал эластик, потому что в него триллион логов посыпалось. Ну или многострадальный пример когда я поменял 2 строчки, и тоже на проде взорвалось в другом месте. Это далеко не 0.001%


                                                                                                    Это вообще к ООП отношения не имеет. В той же Java есть вот такое

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

                                                                                                      0
                                                                                                      А учитывая, что в реальности БД всегда может поломаться, можно сделать вывод что это сигнатура описывает in-memory базу:

                                                                                                      Угу. А если у меня работа с чем-то о чём я не знаю может оно там поломаться или нет? Гадать? Или лезть в тело функции и разбираться?
                                                                                                      И мне всё ещё интересно как будет выглядеть сигнатура у функции, которая каким-либо образом модифицирует содержание прежде чем записать его куда-то?

                                                                                                      А у меня получается, что очень часто это нужно знать. А то был у меня например случай, когда я безобидную функцию вида int x = Sqr(otherInt) написал в цикле, а у меня упал эластик, потому что в него триллион логов посыпалось.

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

                                                                                                      А если это чужая функция, то откуда она знает как в ваш эластик писать?

                                                                                                      Но в ваша функция AddUser разве так написана?

                                                                                                      Ну так ещё раз: это не зависит от ООП или не ООП. В некоторых языках такое указывать нельзя, в других можно, в третьих обязательно.

                                                                                                      Да и вообще в теории вы и ООП язык наверное можете создать в котором надо будет указывать контекст, который может использовать функция. только подозреваю что это опять же мало кому надо.
                                                                                                        0
                                                                                                        Угу. А если у меня работа с чем-то о чём я не знаю может оно там поломаться или нет? Гадать? Или лезть в тело функции и разбираться?
                                                                                                        И мне всё ещё интересно как будет выглядеть сигнатура у функции, которая каким-либо образом модифицирует содержание прежде чем записать его куда-то?

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


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


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

                                                                                                        Ну, где-то в решение она объявлена, в сигнатуре ничего про логи не написано, принимает инт, возвращает инт.


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

                                                                                                        Я про практические языки которые есть на рынке: джава, шарп, хаскель,… То что можно сделать химеру — это конечно никто не спорит, только её нет. А говорить о несуществующем не вижу большого смысла.

                                                                                                          0
                                                                                                          Но обычно так заморчиваться не надо, геморроя много, а толку — не очень.

                                                                                                          То есть получается что цена вопроса это субъективное понимание «геморроя» и «толка». И если кто-то, как например я, считатает что вся эта овчинка в принципе выделки не стоит, то получается что и ООП не проблема? :)

                                                                                                          Ну, где-то в решение она объявлена, в сигнатуре ничего про логи не написано, принимает инт, возвращает инт.

                                                                                                          И как долго вам пришлось выяснять что она всё-таки пишет логи? :)

                                                                                                          Я про практические языки которые есть на рынке: джава, шарп, хаскель,…

                                                                                                          Я бы сказал что на это просто нет достаточного спроса. Если бы он действительно был, то и ЯП бы быстро появились.
                                                                                                            0
                                                                                                            И как долго вам пришлось выяснять что она всё-таки пишет логи? :)

                                                                                                            Да сразу узнал, когда мне написали, что эластик уронился после такого-то коммита.


                                                                                                            Я бы сказал что на это просто нет достаточного спроса. Если бы он действительно был, то и ЯП бы быстро появились.

                                                                                                            На то чтобы такое было в ООП языках — безусловно. Потому что эффекты или должны быть, или нет. Если они есть, то никакой нуллябельности по-умолчанию, экспепшнов по-умолчанию и так далее. Это, кстати, можно видеть в расте.


                                                                                                            Но в существующих языках таких ломающих изменений конечно никогда не будет. А в новых оно понемногу появляется: Раст, Котлин, Свифт, ...

                                                                                                              0
                                                                                                              Всё-таки я с вами не совсем согласен. На мой взгляд и ООП и ФП имеют свои плюсы и свои минусы. И поэтому на мой взгляд одно вряд ли сможет полностью заменить или даже вытеснить другое. По крайней мере не в обозримом будущем уж точно.

                                                                                                              А так да, определённые вещи однозначно проще реализовывать при помощи ООП, а некоторые удобнее при помощи ФП. И поэтому скорее всего их начнут использовать параллельно. Например тот же дотнет вполне себе позволяет «миксить» C# и F#. Пока ещё не особо удобно, но надеюсь что со временем сделают получше.
                                                                                                                +1

                                                                                                                Понимаете, если разбить ООП и ФП по фичам (в смысле, популярные языки), то окажется, что Java это A,B,C,D,
                                                                                                                шарп это A,B,C,E,F,G, а какой-нибудь хаскель это B,C,F,G,H,I,G,K,L,M. Если нарисовать диаграммки Венна, то окажется что они практически пересекаются. Но в этих нюансах и кроется основное различие. И под ООП лично я по крайней мере понимаю те компоненты, которые свойствены Java/C#/..., но не свойствены Haskell/Scala/..., в примере выше это A и E.


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




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

                                                                                0
                                                                                Когда вам надоест писать метакод, и вы спуститесь на уровень пониже, то увидите, сколько вариантов может генерировать одна настоящая функция.

                                                                                Сколькими способами можно например написать такую функцию?
                                                                                fn: Int -> Int
                                                                                Одним? Двумя?… Миллиардом?
                                                                                  0
                                                                                  Когда вам надоест писать метакод, и вы спуститесь на уровень пониже, то увидите, сколько вариантов может генерировать одна настоящая функция.

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


                                                                                  Сколькими способами можно например написать такую функцию?
                                                                                  fn: Int -> Int
                                                                                  Одним? Двумя?… Миллиардом?

                                                                                  (2^32)^(2^32) — количество обитателей легко считается. Общая формула: a -> b имеет b^a обитателей.

                                                                                    0
                                                                                    (2^32)^(2^32) — количество обитателей легко считается. Общая формула: a -> b имеет b^a обитателей.

                                                                                    Выше вы предлагаете генерировать по сигнатуре Int->Int какой-то конкретный вариант из (2^32)^(2^32) штук? Вам не кажется, что вероятность угадать верный вариант слишком мала?

                                                                                      +1

                                                                                      Ну нет, там совсем другая сигнатура написана.

                                                                                  +1
                                                                                  Забавно, как раз недавно видел запись конференции с такой же мыслью
                                                                                0
                                                                                Но ведь так и задумано, абстракция не должна зависеть от реализации. Параметры, которых у метода может не быть, и он при этом не потеряет смысловой нагрузки — это деталь реализации. void Foo() это вероятно пример плохого дизайна абстракции, ну так никакие инструменты от таких косяков не защищают)
                                                                                  +1

                                                                                  Ну так когда мы можем понять РЕАЛИЗАЦИЮ по СИГНАТУРЕ — это и есть IoC, у нас реализация зависит от абстракции (ака сигнатуре), разве нет?). И плохо, когда работает наоборот: посигнатуре вроде мы имеем право работать, но вот нужно посмотреть реализацию и понять, что на самом деле вот так можно делать, а вот так — нельзя.

                                                                                    0
                                                                                    Ну так когда мы можем понять РЕАЛИЗАЦИЮ по СИГНАТУРЕ — это и есть IoC, у нас реализация зависит от абстракции (ака сигнатуре), разве нет?).

                                                                                    Я всё ещё не понимаю как вы там по сигнатуре угадываете конкретную реализацию? Вот есть у меня функция
                                                                                    int DoSomething(int x, int y);
                                                                                    

                                                                                    как мне по одной только сигнатуре понять складывает она там или умножает? Или вообще что-то третье делает?

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

                                                                                    Вам всегда придётся куда-то смотреть как можно делать и как нельзя. Эта информация где-то должа быть записана. И я не вижу принципиальной разницы записана она в теле метода, в описании типа/класса, в какой-то аннотации или даже в форматe вызова функции.
                                                                                      0

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


                                                                                      А вот сигнатура с генериками — совсем наоборот, очень редко когда нельзя сказать что она делает. Например функция:


                                                                                      fn T do_something<T,U>(T x, U y)


                                                                                      всегда возвращает первый аргумент и игнорирует второй (если не зависает паникой или ещё как).


                                                                                      Хотя в шарпе параметричность ломается с помощью typeof, это конечно очень жаль.

                                                                                        0
                                                                                        Как выглядит сигнатура у функции, которая возвращает x+y? А у функции возвращающей х*y? A x в степени y? A x-y? A y-x?
                                                                                          0

                                                                                          ну например так:


                                                                                          fn foo<T: Add>(a: T, b: T) -> T::Output { .. }
                                                                                          
                                                                                          fn bar<T: Mul>(a: T, b: T) -> T::Output { .. }
                                                                                          
                                                                                          fn baz1<R, T: Sub<Rhs=R>>(a: T, b: R) -> T::Output { .. }
                                                                                          
                                                                                          fn baz2<R, T: Sub<Rhs=R>>(a: R, b: T) -> T::Output { .. }
                                                                                            –2
                                                                                            То есть мне теперь надо куда-то лезть и смотреть что это за звери такие «Аdd», «Mul» и «Sub<RHS=R>»? Или как я должен понять что они там делают?

                                                                                            Или скажем как быть если внутри выполняется "(х + у) * (х-у) + (х +х)*(у * 42)… "?
                                                                                              0

                                                                                              Ну вы можете всё тело функции запихнуть так или иначев сигнатуру. Будет у вас тип AddXAndYMultipledByXMinusYPlusDoubleXMultYAnd42 — оно вам нужно? В чем смысл этих расспросов? Давайте я буду у вас спрашивать как что-нибудь в другом языке делается? Это уже переходит рамки приличия.

                                                                                                +1
                                                                                                Будет у вас тип AddXAndYMultipledByXMinusYPlusDoubleXMultYAnd42

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

                                                                                                Например, собрать рядом Expression, а в методе его скомпилить и выполнить — тоже нужно смотреть вне метода, чтобы понять, что он выполнит.

                                                                                                Не уходите, как раз к самому интересному подошли(=
                                                                                                  0
                                                                                                  Т. е., чтобы добиться нужного уровня информативности (кажется все началось с необходимости понять, что происходит внутри метода, не заглядывая внутрь), надо второй раз «метаданными» написать реализацию метода?

                                                                                                  Да.


                                                                                                  Поэтому так никто и не делает.


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


                                                                                                  И для того чтобы понять что делает функция foo : a -> a не нужны никакие сложные типы и дублирование тела в сигнатуре. Перефразируя, парметричность говорит что чем более абстрактная функция, тем меньше множество возможных её реализаций. А значит тем больше надежность и лучше работает интуиция, что функция может делать, а чего — нет.


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




                                                                                                  Ну вот простой пример пусть будет функция:


                                                                                                  bar : [a] -> [a]
                                                                                                  bar xs = ...

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


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

                                                                                                  +1
                                                                                                  Извините, но вы же сами вроде бы написали следующее:
                                                                                                  Перефразирую: как не читая тела функции понять, что она может делать, а что — нет?


                                                                                                  Вот я всё и пытаюсь понять как такое должно работать. И получается что работает такое в ФП только если кто-то был настолько добр что поместил тело функции в сигнатуру. Что по вашим же словам обычно всё равно никто не делает. Плюс это самое тело функции вам всё равно надо читать, но просто в другом месте.

                                                                                                  То есть бенефит от всего этого есть, но пожалуй только для не особо сложных функций. А если функция более-менее сложная, то вам и в ФП всё равно придётся лезть в её «тело» и разбираться уже там. Но взамен у вас сигнатуры заметно сильнее разбухают…

                                                                                                  P.S.Ну или вот напишу я вам какой-нибудь, автодок, который автоматом копипэйстит тело функции в её комментарии. И сможете вы в ООП точно так же «понимать что делает функция не читая её тела».

                                                                                                  P.P.S. И самое главное получается что каждый раз когда я буду менять реализацию своей функции, я должен буду менять её сигнатуру чтобы люди могли «понимать что она делает не читая её тела». И будет у меня при каждом багфиксе изменение сигнатур у каждого метода, который я хоть как-то тронул. Не сказал бы что такой вариант меня сильно радует…
                                                                                                    0
                                                                                                    То есть бенефит от всего этого есть, но пожалуй только для не особо сложных функций. А если функция более-менее сложная, то вам и в ФП всё равно придётся лезть в её «тело» и разбираться уже там. Но взамен у вас сигнатуры заметно сильнее разбухают…

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


                                                                                                    P.S.Ну или вот напишу я вам какой-нибудь, автодок, который автоматом копипэйстит тело функции в её комментарии. И сможете вы в ООП точно так же «понимать что делает функция не читая её тела».

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


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

                                                                                                    А ещё если смените String на Int то тоже код ломается, вот грустно. А у питонистов отлично — просто начал использовать переменную как число и ничего не поломалось. Красота.

                                                                                                      0
                                                                                                      Чем сложнее функция, тем наборот проще, потому что каждый констрейнт дает информацию о том, что она делает. По 3-4 консстрейнтам уже можно практически точно сказать, что делает функция.

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

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

                                                                                                      Проверяться не будет. Но я могу написать это таким образом что комментарий всегда будет один в один выглядеть как тело функции. И если функция не компилируется, то и комментария не будет. Но да, это я дoлжен проявить желание такое сделать и заставить меня вы не можете. Как впрочем и в ФП вы не можете никого заставить писать грамотные сигнатуры.

                                                                                                      А ещё если смените String на Int то тоже код ломается, вот грустно.

                                                                                                      Да, ломается. Но по моему опыту в «ООП языках» тело функции меняется гораздо чаще чем её сигнатура. И как раз таки смена сигнатур это обычно breaking changes и этого стараются по возможности избегать.
                                                                                                        0
                                                                                                        Я бы сказал что они скорее дают информацию о том что она в принципе не может делать и таким образом сужают «область поиска». Это полезно, но «покупается» за счёт «разбухающих сигнатур».

                                                                                                        Ну они не сильно-то разбухают. Особенно в наш век IDE


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

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


                                                                                                        Да, ломается. Но по моему опыту в «ООП языках» тело функции меняется гораздо чаще чем её сигнатура. И как раз таки смена сигнатур это обычно breaking changes и этого стараются по возможности избегать.

                                                                                                        В данном случае, изменение сигнатуры это изменение требований. И я лучше получу ломающее изменение, чем человек молча напишет default(T) внутри тела, не меняя функцию, и у меня потом будет поломка из-за 0/null (был прецедент)

                                                                                                        0
                                                                                                        А у питонистов отлично — просто начал использовать переменную как число и ничего не поломалось.
                                                                                                        Вы python и javascript/php не перепутали? Это в javascript/php можно сравнивать всё со всем и в результате нет тразитивности ни у ==, ни ну <
                                                                                                          0

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

                                                                                                  0
                                                                                                  Для меня это выглядит сомнительно, потому что на моем личном опыте те ошибки, которые я чаще всего встречал, были вызваны именно ошибками в семантике, а не в типах. Неправильный формат сообщения, забытый вызов метода внешнего окружения, неправильный алгоритм подсчёта, и тд. Ошибки с типами это минимум, возможно, потому что больше я работал с Java и Objective-C. И в 99% случаев действительно мне либо вообще не интересно знать, что делает компонент, либо я закладываю, что каждый мой код работает с 5-10 реализациями одного интерфейса, и если я даже сделаю интерфейс типа Add, чтобы по сигнатуре догадываться, что там может быть только сложение чисел, у меня остальные 9 реализаций скажут «А нам то что делать? У нас не сложение используется». Сделать в сигнатуре разрешение на сложение, вычитание, умножение, деление, чтобы можно было все нужные кейсы покрыть? Сигнатура становится монстроуозной, а по ней уже не скажешь, что конкретно она делает. Сомневаюсь, что даже на языках с навороченной системой типов можно описать, как происходит алгоритм расчёта, какие числа и в какой последовательности мы используем операторы, а именно в этом чаще всего я встречал косяки.
                                                                                                  Мне кажется, в этом и прелесть ООП, что в нем класс не берет на себя дополнительную обязанность знать, что и как делает его зависимость. Ему не должно быть интересно, ходит ли его зависимость в сеть, пишет в лог, генерирует рандомные данные, отдаёт всегда число 666 или вообще завершает приложение с ошибкой. То, какую реализацию программист положил в объект, с тем он и будет работать.
                                                                                                  И почему такое отношение к code conventions? По моему, это самое разумное, что программист сам на себя возлагает определённые ограничения, потому что не существует системы, которая запретит м**аку быть м**аком. Назовите мне хоть одну систему или свод правил, которая бы смогла запретить безалаберному человеку или вредителю делать его дело плохо. Здравый смысл необходим человеку всегда, его не получится задвинуть на дальнюю полку. А если человек адекватный, то даже в языке без nullable можно сделать себе жизнь комфортной простой договоренностью.
                                                                                                    –1
                                                                                                    Для меня это выглядит сомнительно, потому что на моем личном опыте те ошибки, которые я чаще всего встречал, были вызваны именно ошибками в семантике, а не в типах. Неправильный формат сообщения, забытый вызов метода внешнего окружения, неправильный алгоритм подсчёта, и тд.

                                                                                                    Всё это можно и нужно проверять типами. Типизированная форматирующая строка это вообще один из примеров для начинающих идрисистов. Ну и немного теории если есть сомнения.


                                                                                                    C. И в 99% случаев действительно мне либо вообще не интересно знать, что делает компонент, либо я закладываю, что каждый мой код работает с 5-10 реализациями одного интерфейса, и если я даже сделаю интерфейс типа Add, чтобы по сигнатуре догадываться, что там может быть только сложение чисел, у меня остальные 9 реализаций скажут «А нам то что делать? У нас не сложение используется». Сделать в сигнатуре разрешение на сложение, вычитание, умножение, деление, чтобы можно было все нужные кейсы покрыть? Сигнатура становится монстроуозной, а по ней уже не скажешь, что конкретно она делает. Сомневаюсь, что даже на языках с навороченной системой типов можно описать, как происходит алгоритм расчёта, какие числа и в какой последовательности мы используем операторы, а именно в этом чаще всего я встречал косяки.

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


                                                                                                    Никто же не расстраивается, что List в сишарпе реализует пару десятков интерфейсов?


                                                                                                    Мне кажется, в этом и прелесть ООП, что в нем класс не берет на себя дополнительную обязанность знать, что и как делает его зависимость.

                                                                                                    Как раз ООП более ограниченное. Если у вас есть синхронный метод вы в наследнике не сможете сделать его асинхронным никак. Потому что сигнатура жестко фиксируется родителем.


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

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


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

                                                                                                    Нет

                                                                                                      0
                                                                                                      Всё это можно и нужно проверять типами. Типизированная форматирующая строка это вообще один из примеров для начинающих идрисистов. Ну и немного теории если есть сомнения.

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

                                                                                                      В вашем предыдущем сообщении речь шла о понимании, что происходит внутри метода. У метода в аргументах 2 Num, на выходе один, что он конкретно делает, сказать точно уже нельзя, потому что это не Add, над которым можно проводить только одну операцию. И в реальных задачах обычно идет речь не про оперирование числами, а, например, про отображение списка ячеек, каждая со своими данными, со своей логикой, со своей реакцией на события, и тд. Заложить в сигнатуре общего интерфейса все возможные варианты поведения, чтобы достоверно знать, что происходит внутри любой из имплементаций интерфейса, это как по вашему должно выглядеть?
                                                                                                      А если человек адекватный, то даже в языке без nullable можно сделать себе жизнь комфортной простой договоренностью.

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

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

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

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


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


                                                                                                        В вашем предыдущем сообщении речь шла о понимании, что происходит внутри метода. У метода в аргументах 2 Num, на выходе один, что он конкретно делает, сказать точно уже нельзя, потому что это не Add, над которым можно проводить только одну операцию.

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


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

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


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

                                                                                                        Я говорю про то что у вас нет ошибок "'number' doesn't have property 'length'", а не то что это сильвербулет.


                                                                                                        Я не знаю, в каком языке можно унаследоваться/обернуть синхронный объект/метод с сохранением сигнатуры и сделать его асинхронным.

                                                                                                        Любой язык с типами высших порядков. Скала, как пример.

                                                                                                          0

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


                                                                                                          « Любой язык с типами высших порядков. Скала, как пример.»


                                                                                                          И как это выглядит? Я просто такого ни разу не встречал.

                                                                                                            0
                                                                                                            Вот я о том и говорил, что все системой типов не покрыть, только простые вещи типа передачи строки вместо числа

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


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


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


                                                                                                            И как это выглядит? Я просто такого ни разу не встречал.

                                                                                                            Ну что-то в таком духе:


                                                                                                            trait MyInterface[Self]:
                                                                                                              type F[A]
                                                                                                              def (item: Self) getSomething() (using Monad[F]): F[Int]
                                                                                                            
                                                                                                            class SyncInterface
                                                                                                            class AsyncInterface
                                                                                                            
                                                                                                            given syncInstance as MyInterface[SyncInterface]:
                                                                                                              type F[A] = Id[A]
                                                                                                            
                                                                                                              def (item: SyncInterface) getSomething()(using Monad[Id]): Id[Int] = 42
                                                                                                            
                                                                                                            given asyncInstance as MyInterface[AsyncInterface]:
                                                                                                              type F[A] = IO[A]
                                                                                                            
                                                                                                              def (item: AsyncInterface) getSomething()(using Monad[IO]): IO[Int] = () => 42 

                                                                                                            Я использую в качестве затычки IO который просто синхронный коллбек, но на самом деле там должно быть что-то из библиотеки cats, например вот это: https://typelevel.org/cats-effect/typeclasses/async.html


                                                                                                            Работать будет точно так же как и пример.

                                                                                                              0
                                                                                                              Про продвинутую систему типов не знаю, почему-то пока что я не настолько сильно замечаю, что с переходом от того же Objective-C к Swift, где система типов гораздо сильнее, у меня резко уменьшилось количество ошибок. Но видимо это разнится от человека к человеку.

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

                                                                                                                Кроме продвинутой системы типов ещё некоторое значение имеет прокладка между монитором и креслом :)


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


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

                                                                                                                Но это будет не то же самое. В случае выше у вас две реализации имуют типы Id[A] и IO[A], ну или Sync<T> и Async<T> соответственно. А третья реализация может например возвращать Option — тоже полезный кейс.


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


                                                                                                                Ну и если посмотреть на это философски, то это все равно что у вас есть две реализации функции, одна должна возвращать число, а другая — строку, и вы делаете общий тип — строку, а ту которая возвращает число вы просто пишете как myint.toString(). Можно ли так сделать? Да, строки более "Общие" чем числа (как асинк более общий, чем синк). Но тут во-первых эстетически это грязь, а во-вторых вам нужно теперь помнить, где числа а где нет, чтобы знать, когда в число парсить (нам ведь нужно это число где-то получить, правда?). В случае асинка это означает, что мы должны помнить, когда можно заблокироваться, а когда нельзя, или везде писать асинк-авейт, даже там, где мы передали реализацию которая на самом деле синхронная.


                                                                                                                Ну и наконец, писать Task.FromResult/Promise.resolve на каждый чих утомляет.

                                                                                                                  –1
                                                                                                                  Кроме продвинутой системы типов ещё некоторое значение имеет прокладка между монитором и креслом :)

                                                                                                                  Вот я о том же) Eсли человек и его команда хотят облегчить себе жизнь, то можно ведь выработать определенные правила, например, для работы с теми же опциональными значениями. Человек + тот же код ревью будут заменять собой правила компилятора, не 100% идеально конечно, но и не на уровне пустой траты времени, и к этому еще и смогут когда нужно это игнорировать, если это требует ситуация, без сложной возни с системой ограничений языка.

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

                                                                                                                  Не совсем наверное понимаю суть различия. Вот у нас есть некий
                                                                                                                  interface A {
                                                                                                                      int sum(int, int);
                                                                                                                  }
                                                                                                                  

                                                                                                                  Для асинхронности мы делаем
                                                                                                                  interface B {
                                                                                                                      void sum(int, int, Completion);
                                                                                                                  }
                                                                                                                  

                                                                                                                  реализуем этот B, передадим ему реализацию A и внутри напишем что нибудь типа
                                                                                                                  void sum(int x, int y, Completion c) {
                                                                                                                      c(a.sum(x, y));
                                                                                                                  } 
                                                                                                                  

                                                                                                                  Да, сигнатура конечно поменяется, но я так понимаю, что и в случае sync async мы вынуждены поменять сигнатуру вызываемой функции. У нас ведь шла речь, что в ООП сигнатура функции изменяется, а в ФП функция становится асинхронной, но сохраняет изначальную сигнатуру. Или я не так понял пример на Scala?
                                                                                                                    +1
                                                                                                                    Вот я о том же) Eсли человек и его команда хотят облегчить себе жизнь, то можно ведь выработать определенные правила, например, для работы с теми же опциональными значениями. Человек + тот же код ревью будут заменять собой правила компилятора, не 100% идеально конечно, но и не на уровне пустой траты времени, и к этому еще и смогут когда нужно это игнорировать, если это требует ситуация, без сложной возни с системой ограничений языка.

                                                                                                                    Тем не менее, ни с какими договоренностями я не видел чтобы люди избавились от nullref exception. И атрибуты вешали, и договаривались называть TryXXX если нулл может вернутся — все равно не помогало. А вот с Option явным такого не случается.


                                                                                                                    Не совсем наверное понимаю суть различия. Вот у нас есть некий

                                                                                                                    Ну так ваш completion очень похож на TaskCompletionSource (ну или Promise.resolve из жс), тот же асинк, только в профиль.


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


                                                                                                                    С одним интерфейсом вы можете писать код, который работает как для синронных, так и для асинхронных вариантов. В вашем случае вы можете или с A работать, или с B, но вы не можете написать одну функцию которая работает как с А, так и с B одинаково.

                                                                                                                      0
                                                                                                                      Тем не менее, ни с какими договоренностями я не видел чтобы люди избавились от nullref exception. И атрибуты вешали, и договаривались называть TryXXX если нулл может вернутся — все равно не помогало. А вот с Option явным такого не случается.

                                                                                                                      Мне кажется, что и в языках с убер типизацией люди никуда не ушли от багов. Количество поуменьшилось, но все равно есть + добавились другие проблемы, увеличение времени сборки, сложный обход системы типов, если нужно в каком-то случае что-то подкрутить, прочее. Не знаю точно, но ощущение такое есть) Или действительно с той же Scala баги практически исчезли?

                                                                                                                      Ну так ваш completion очень похож на TaskCompletionSource (ну или Promise.resolve из жс), тот же асинк, только в профиль.

                                                                                                                      Не спорю, просто хотел понять фразу про «ограничение в ООП». Если в ООП тоже можно превратить синхронный вызов метода в асинхронный, то в чем его ограниченность?
                                                                                                                        0
                                                                                                                        Мне кажется, что и в языках с убер типизацией люди никуда не ушли от багов. Количество поуменьшилось, но все равно есть + добавились другие проблемы, увеличение времени сборки, сложный обход системы типов, если нужно в каком-то случае что-то подкрутить, прочее. Не знаю точно, но ощущение такое есть) Или действительно с той же Scala баги практически исчезли?

                                                                                                                        Programming Defeatism: No technique will remove all bugs, so let's go with what worked in the 70s.


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


                                                                                                                        И да, целые классы багов ушли. Я вот не припомню ни одного memory-safety бага в сишарп проектах на которых я был. Совпадение?


                                                                                                                        Не спорю, просто хотел понять фразу про «ограничение в ООП». Если в ООП тоже можно превратить синхронный вызов метода в асинхронный, то в чем его ограниченность?

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

                                                                                                                          0
                                                                                                                          Programming Defeatism: No technique will remove all bugs, so let's go with what worked in the 70s.

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

                                                                                                                          И да, целые классы багов ушли. Я вот не припомню ни одного memory-safety бага в сишарп проектах на которых я был. Совпадение?


                                                                                                                          Согласен, не исправить. Мой посыл больше про то, насколько от усиления системы типов усложняется язык, увеличивается количество ограничений, замедляется время сборки и насколько меньше становится багов. Добавление типизации в той же Java решило много проблем в сравнении с JavaScript и незначительно усложнило язык, а вот дополнительное наращивание типизации для меня выглядит так, что мы больше усложняем себе жизнь новыми ключевыми словами, ограничениями с выводом типов, нюансами при работе с типами, чем решаем насущных проблем. Огромный пласт ошибок именно в логике работы программы, когда человек не так посчитал, забыл что-то вызвать, вызвал дважды, вызвал в неправильном порядке, запросил и сильно нагрузил базу, и тд, и это система типов не может покрыть. Никто не предлагает возвращаться в 70ые, но и пытаться задекларировать все, мне кажется, тоже утопичная идея, которая больше усложнит работу, чем реально даст пользу.

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

                                                                                                                          Можно, например, сделать у A дженерик для аргументов и результирующего значения, и у B тоже. Можно будет писать нужные реализации А для операций над T, строки, числа, и тд и тп. Единственному классу, реализующему асинхронную работу, будет без разницы, с какой именно реализацией А работать.
                                                                                                                            +1
                                                                                                                            Добавление типизации в той же Java решило много проблем в сравнении с JavaScript и незначительно усложнило язык, а вот дополнительное наращивание типизации для меня выглядит так, что мы больше усложняем себе жизнь новыми ключевыми словами, ограничениями с выводом типов, нюансами при работе с типами, чем решаем насущных проблем.

                                                                                                                            Нет, это классический парадокс блаба: вы знаете Java поэтому смотрите на JavaScript сверху вниз и видите, как фичи джавы помогают. А когда вы смотрите "Наверх", то выидите "странные языки", которые возможно такие же мощные как джава, но с какими-то странными прибабахами и сложностями на ровном месте. И зачем?! Ведь я знаю, как то же самое сделать на джаве, где ничего этого нет.


                                                                                                                            Это решается только кругозором. Ну или не решается, и человек до конца жизни уверен, что нашел идеальный инструмент.


                                                                                                                            Можно, например, сделать у A дженерик для аргументов и результирующего значения, и у B тоже. Можно будет писать нужные реализации А для операций над T, строки, числа, и тд и тп. Единственному классу, реализующему асинхронную работу, будет без разницы, с какой именно реализацией А работать.

                                                                                                                            Ну это похоже на правду, за исключением того, что без типов высших порядоков вы не сможете написать такой генерик. Ну не выразить на сишарпе или джаве тип T<i32>, чтобы пользователь мог сам выбрать реализацию. Поэтому и не получится Sync<i32> или Async<i32> выбрать по месту.




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

                                                                                                                              0

                                                                                                                              « Нет, это классический парадокс блаба: вы знаете Java поэтому смотрите на JavaScript сверху вниз и видите, как фичи джавы помогают. А когда вы смотрите "Наверх", то выидите "странные языки", которые возможно такие же мощные как джава, но с какими-то странными прибабахами и сложностями на ровном месте.»


                                                                                                                              Ну вообще я сначала писал на JavaScript, только потом начал на Java, затем на Objective-C, а потом перешёл на Swift с более навороченной системой типов, чем в Java. Но почему-то уровень типизации Objective-C/Java мне показался золотой серединой, чтобы и опечаток/ошибок с типами было по минимуму, и работать было по-прежнему комфортно. Более навороченные дженерики вроде и хорошо, но порой начинается война с тем, как объяснить компилятору своё намерение, если накрутил какой-нибудь абстрактный компонент.


                                                                                                                              Но в целом я понял вашу идею, спасибо, что разъяснили про Scala и ваши взгляды на типизацию. С «ограниченностью ООП» холиварный был вброс, но не хочу его начинать, согласен разве что с тем, что в мейнстримовых языках подобные задачи решатся чуть большим количеством кода, но для меня это не ограничение парадигмы, а недоработка разработчиков языков, которые фокусируются на других вещах.
                                                                                                                              Спасибо вам за дискуссию, мне было очень интересно узнать ваши взгляды. И спасибо за материал про типизацию форматирования строки, до этого я не знал, что под это подводят доказательную базу.

                                                                                                                                +1
                                                                                                                                Ну вообще я сначала писал на JavaScript, только потом начал на Java, затем на Objective-C, а потом перешёл на Swift с более навороченной системой типов, чем в Java. Но почему-то уровень типизации Objective-C/Java мне показался золотой серединой, чтобы и опечаток/ошибок с типами было по минимуму, и работать было по-прежнему комфортно. Более навороченные дженерики вроде и хорошо, но порой начинается война с тем, как объяснить компилятору своё намерение, если накрутил какой-нибудь абстрактный компонент.

                                                                                                                                Ну свифт не особо мощнее джавы, особенно учитывая Arc вместо полноценного гц, с ним может казаться даже менее высокоуровневым. Дальше по спектру это скорее всякие скала/хаскель, или Rust в стиле Томаки.


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

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


                                                                                                                                То есть я не ругал, а просто констатировал некоторое свойство некоторых существующих языков (но не парадкигмы как таковой, к слову).


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

                                                                                                                                Взаимно, было приятно поболтать)

                                                                                                                                  +1
                                                                                                                                  Как в хаскелле нельзя выразить некоторые вещи которые можно в идрисе — ну просто язык не настолько расширяем чтобы это работало.
                                                                                                                                  Тут есть ещё некая схожесть с тьюринговская трясиной.

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

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

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

                                                                                                                                  Хороший пример — метапрограммирование в C++. Появилось оно ещё в C++98 (причём оно в язык было не добавлено, а открыто… во время стандартизации этому уже внимание чуть-чуть уделили), однако «простые смертные» могут им пользоваться только начиная с C++17 — потому что только там есть такие вещи, как fold expression и constexpr if. С ними метапрограммирование начинает быть похожим на обычное программирование, в то время, как до того — у вас получался, плюс-минус, «типа-как-бы-Lisp-посреди-C++». Который «осиливали» немногие…
                                                                                                                                    0

                                                                                                                                    Так шаблоны это не типы, это именно что шаблоны, кодген по, собственно, шаблону. Можно ли сгенерировать что-то что будет тайпчекаться? Можно. Но это не типы, и удобство соответствующее.

                                                                                                                                      0
                                                                                                                                      Так шаблоны это не типы, это именно что шаблоны, кодген по, собственно, шаблону.
                                                                                                                                      Вы либо не в курсе того, что такое шаблоны в C++, либо передёргиваете. Вот какой-нибудь Maybe — это тип или нет? Ну, по крайней мере обычно считается, что да. А в C++ такая же, по сути, вещь — это шаблон. Только там была забавная фича — было разрешено делать специализацию для конкретного типа. Ну там, чтобы optional<bool> сделать эффективнее чем с помощью стандартной схемы. Это сделало язык описания типов тьюринг-полным, что сразу же «приспособили к делу». В Haskell (ну… в GHC) есть полноценное метапрограммирование, так что это всё не очень нужно. А в C++ есть даже целые библиотеки, позволяющие на этом всём программировать…
                                                                                                                                        0

                                                                                                                                        Maybe — тайплевел функция. В отличие от шаблона это полноценный объект.


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

                                                                                                                                          +1

                                                                                                                                          Шаблоны плюсов и макросы это абсолютно разные вещи.


                                                                                                                                          Макросы си работают тупо на уровне текста, макросы раста на уровне AST, а шаблоны плюсов на уровне типов.


                                                                                                                                          Например, std::optional<T> это полноценный тип. А вот my_macro!(T) это новый кусок кода который нельзя вставить никуда кроме корня файла.


                                                                                                                                          Как соотносятся дженерики раста и шаблоны плюсов я ответить не могу. Пока внятных объяснений от местных теоретиков я тоже не видел :)

                                                                                                                                            0
                                                                                                                                            Макросы си

                                                                                                                                            А я не про макросы в Си. Я про макросы в расте. Которые куда ближе к шаблонам, нежели что-то другое.


                                                                                                                                            std::optional<T>

                                                                                                                                            Не знаю глубоко плюсов, но насколько я знаю, шаблоны это шаблоны, а не типы, они даже не чекаются если вы их не инстанцировали, а если инстанцировали то проверяется уже результат раскрытия, а не что-то другое. Это сильно отличается от понятия "типа".

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

                                                                                                                                              А вот так — уже нет.

                                                                                                                                              а если инстанцировали то проверяется уже результат раскрытия, а не что-то другое
                                                                                                                                              Таки чекается «что-то другое»: шаблоны могут в несколько этапов раскрываться и на каждом этапе чекается то, что не зависит от реализации шаблона. Собственно идея концептов (полноценных, который должны были быть в C++11, а не та версия, которая дожила до C++20) была как раз в том, чтобы они могли чекаться вообще на этапе объявления. Стали бы они в этом случае «полноценными типами» в вашем мире или нет?

                                                                                                                                              В Haskell подобная конструкция называется полиморфными типами… и если честно, большой разницы я не вижу: точно также всё чекается когда вы вот это вот пытетесь из функции, оперирующей «неполиморфными» типами проверяется…
                                                                                                                                                0

                                                                                                                                                Typeable — это способ получать в рантайме информацию о типах, а полиморфизм генериков не имеет ничего общего с шаблонами. Вот сводная табличка от майкрософта: https://docs.microsoft.com/en-us/cpp/extensions/generics-and-templates-visual-cpp?view=vs-2019


                                                                                                                                                И хотя она касается сишарпа, параметрический полиморфизм в хаскелле работает так же, пусть и чуть-чуть богаче с rank-2, type family, undecidable instances и прочими приколами.

                                                                                                                                                0

                                                                                                                                                Я тоже не специалист, но у меня есть ощущение что темплейты это такие тайплевел функции только на (условно) js, т.е. чекаются во время вызова (инстанциирования).

                                                                                                                                        0
                                                                                                                                        Хороший пример — метапрограммирование в C++. Появилось оно ещё в C++98 (причём оно в язык было не добавлено, а открыто… во время стандартизации этому уже внимание чуть-чуть уделили), однако «простые смертные» могут им пользоваться только начиная с C++17

                                                                                                                                        Ну, не знаю… Я вот себя не считаю «небожителем», и с даже специалистом по C++, но вот template'ы и использовал, и свои писал ещё задолго до 2017 года.
                                                                                                                                        Скорее всего тут дело в том, что это «метапрограммирование» в C++98 имело своего предшественника ещё в C — директиву препроцессора #define с параметрами. Которую мне тоже пришлось в свое время освоить, потому как использовалась она очень широко. Ну, а с template уже хотя бы некоторые вещи можно было делать по аналогии. Но некоторые другие (типа классов-функторов для STL) — таки да, пришлось осваивать.
                                                                                                                                        Ну, а ещё эта аналогия и приобретенные ранее привычки очень помогали искать ошибки, которых тогда было в количестве — ибо в старых стандартах дозволялись многие вещи, которые потом, после разворачивания шалона, не компилировались с малопонятными ошибками.
                                                                                                    0
                                                                                                    Я не понимаю, почему. В расте какие-то другие генерики, не такие, как шаблоны в плюсах? Почему из одинаковости _типа_ следует одинаковость _значения_?
                                                                                                      0

                                                                                                      Да, другие. В safe Rust внутри функции с такой сигнатурой у вас нет ни одной возможности получить валидное значение типа T кроме переданного x.

                                                                                                        0
                                                                                                        Какова же должна быть сигнатура метода сложения T x и T y (T z в итоге, допустим)?
                                                                                                          –1
                                                                                                          fn add<T: Add>(x: T, y: T) -> T
                                                                                                            0
                                                                                                            А, то есть если у Т явно не задан никакой трейт (я верно называю?) (concept/constraint в с++), то мы с ним ничего не можем сделать, ни скопировать, ни сложить?
                                                                                                            А почему мы не можем создать и вернуть пустой Т, нужен DefaultConstructible трейт?
                                                                                                              0

                                                                                                              Да, если для T не задано констрейнтов мы ничего не можем сделать. Для создания пустого нужен констрейнт T : Default, всё верно. Для того чтобы вернуть значение полученное из какой-нибудь захардкоженной числовой константы понадобится констрейнт
                                                                                                              T : From<i32>
                                                                                                              , ну и так далее.


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

                                                                                                                0
                                                                                                                Именно поэтому она даёт столько пищи для размышлений


                                                                                                                Хорошо, допустим у меня есть функции lower_bound и upper_bound — у них требования на Т одинаковые (наличие оператора< и… всё?). Да, исходя из того, что требуемый контейнер/рэнж должен быть RandomAccess, я могу по сигнатуре догадаться что это бинарный поиск, но какой из двух? Или я уже придираюсь и хочу слишком много?
                                                                                                                Просто мне абстрактно кажется, что есть достаточно большой класс функций с одинаковыми требованиями на Т/U где не заглянув в код/не посмотрев имя функции (а там doWork или ProcessValues), нельзя догадаться о том, что функция делает.
                                                                                                                  0

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


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


                                                                                                                  Ну и так далее.


                                                                                                                  Что до примера, то я не очень понял. Возьмем хаскель, там есть тайпкласс Bounded который задаёт две функции minBound/maxBound


                                                                                                                  Если я увижу функцию вида


                                                                                                                  doWork : (Bounded a, Ord a) => Vector a -> Vector a

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

                                                                                                                    0
                                                                                                                    Может вернуть нулл (нужно проверять всегда результат)

                                                                                                                    В С# для этого добавили Nullable References.

                                                                                                                    Может вернуть ошибку (нужно обрабатывать такую возможность)

                                                                                                                    В Jave поддрживается и надеюсь что рано или поздно добавят и в C#. А пока да, при необходимости приходится полагаться на всякие exception reflector'ы.

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

                                                                                                                    Это да, «нативно» такое те же C# с Java не поддерживают. Но на мой взгляд для такого есть coding conventions. Они не панацея и «работают» гораздо хуже, но жить можно.
                                                                                                                      0
                                                                                                                      В С# для этого добавили Nullable References.

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


                                                                                                                      В Jave поддрживается и надеюсь что рано или поздно добавят и в C#. А пока да, при необходимости приходится полагаться на всякие exception reflector'ы.

                                                                                                                      Ну так оно фигово работает в таком виде. Вот скажите, как мне написать сигнатуру такой функции:


                                                                                                                      void Foo(Action action) {
                                                                                                                         action();
                                                                                                                      }

                                                                                                                      Где Foo бросает ровно те же исключения, что и action? Насколько мне известно, в Java такое записать невозможно.


                                                                                                                      Это да, «нативно» такое те же C# с Java не поддерживают. Но на мой взгляд для такого есть coding conventions. Они не панацея и «работают» гораздо хуже, но жить можно.

                                                                                                                      Так так со всем. Нет нулляблов — ну ладно, будем конвенциями не забывать проверять на нулл. Эксепшны? Будем конвеншнами указывать, что где может выброситься (например, в проектах Project.Core/Project.Common могут бросаться только *BusinessException). И так далее.


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

                                                                                                                        0

                                                                                                                        А в чём по вашему заключается кривизна работы nullable references?


                                                                                                                        И да, в куче языков чего-то нет и не хватает. Ну так и Рим не за один день строился.

                                                                                                                          0

                                                                                                                          1 нельзя написать T? FirstOrNull(IEnumerable<T> source), например.
                                                                                                                          2 По той же причине нельзя написать структуру данных, которая возвращает такую функцию, например, мне нужно было такой интерфейс реализовать:


                                                                                                                          interface ISettings<T> 
                                                                                                                          {
                                                                                                                            T? GetSettings();
                                                                                                                          }

                                                                                                                          3 null propagation не работает в половине случаев: при вызове конструктора, при вызове статических методов
                                                                                                                          4 ...


                                                                                                                          короче, список проблем существенный, можно ещё продолжать и продолжать. И если null propagation — ну ладно, мы не гордые, напишем руками. то невозможность такой интерфейс сдеалть очень расстроила.

                                                                                                                            0

                                                                                                                            Вроде для этого приспособили атрибуты:


                                                                                                                            [return: MaybeNull]
                                                                                                                            T FirstOrNull(IEnumerable<T> source)
                                                                                                                              0

                                                                                                                              Ну так что это, как не костыли? Плюс оно не будет правильно работать. Например, из моих персональных экстеншнов:


                                                                                                                              public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class =>
                                                                                                                                  source.Where(x => x is {})!;
                                                                                                                              
                                                                                                                              public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : struct =>
                                                                                                                                  source.Where(x => x is {}).Select(x => x.GetValueOrDefault());

                                                                                                                              Тут даже реализация отличается, не получится атрибутом это выразить. Ну или делать if typeof(..) { .. }

                                                                                                                                0
                                                                                                                                Ну так что это, как не костыли?

                                                                                                                                Не совсем костыли. Под капотом оно все равно в атрибуты разворачивается.


                                                                                                                                Тут даже реализация отличается, не получится атрибутом это выразить. Ну или делать if typeof(..) {… }

                                                                                                                                Увы, дженерики C# для такого не предназначены. Вам надо в C++ лезть.


                                                                                                                                Конечно, есть костыль, который позволяет делать перегрузку для случаев, когда аргумент — class и когда — struct, но я не рекомендую им пользоваться.

                                                                                                                                  0

                                                                                                                                  для чего не предназначены? почему в расте я могу написать:


                                                                                                                                  fn while_not_null<T>() -> Option<T>

                                                                                                                                  в скале могу


                                                                                                                                  def WhileNotNull[T](): Maybe[T]

                                                                                                                                  а в сишарпе на таких же генериках — не могу? Где принцииальное отличие?

                                                                                                        0

                                                                                                        Если сравнение с шаблонами то вот простой пример:


                                                                                                        public static class C
                                                                                                        {
                                                                                                            public static void DoIt<T>(T t)
                                                                                                            {
                                                                                                                ReallyDoIt(t);
                                                                                                            }
                                                                                                            private static void ReallyDoIt(string s)
                                                                                                            {
                                                                                                                System.Console.WriteLine("string");
                                                                                                            }
                                                                                                            private static void ReallyDoIt<T>(T t)
                                                                                                            {
                                                                                                                System.Console.WriteLine("everything else");
                                                                                                            }
                                                                                                        }

                                                                                                        Вызов C.DoIt("Hello") выведет "everything else" в сишарпе и "string" если это переписать на плюсовые шаблоны




                                                                                                        Но я имел в виду то, что в сишарпе есть хак для того чтобы ломать параметричность:


                                                                                                        void Foo<T>() 
                                                                                                        {
                                                                                                           if (typeof(T) == typeof(int)) {
                                                                                                              Console.WriteLine("AZAZAZA");
                                                                                                           }
                                                                                                           else {
                                                                                                              Console.WriteLine("Some generic code");
                                                                                                           }
                                                                                                        }

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

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

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

                                                                                                        0
                                                                                                        Имхо сигнатура и абстракция — ортогональные вещи, тут есть два предельных случая:
                                                                                                        — вся инфа передается через сигнатуру
                                                                                                        — вся инфа передается через контекст и новый граф объектов собирается перед каждым вызовом метода
                                                                                                        И то и другое в общем случае ведет к несопровождаемым системам, поэтому задача программиста тут — выбрать такую сигнатуру, которая будет принимать минимальный набор параметров, дающий и итоге клиентский код, легко поддающийся сопровождению. Во многих случаях такое решение принять легко, например я не видел случаев чтобы люди из бизнес логики в DAO передавали коннекшн до БД аргументом метода) Во многих других сложно. В общем апишка должна быть настолько абстрактной, насколько это возможно, но не более того. Проблема в том, что так умеют с первого раза только эльфы, но мы ведь сейчас про теорию)
                                                                                                          0
                                                                                                          Понял что вы имели ввиду, полностью валидный кейс, но он ведет к следующему: предположим что мы передаем в метод коннекшн до БД и делаем это через параметры. Тогда вызывающий код должен про этот коннекшен знать, скорее всего он не сам его создает, следовательно он сам должен его откуда-то получить. В общем все аргументы, которые не материализуются непосредственно перед вызовом метода, а скажем определяются на этапе создания графа объектов, будут путешествовать по всему этому графу сверху вниз. Обычно когда такая ситуация возникает, чтобы с ней бороться создаются параметры типа Context в которые напихиваются все данные подряд, по сути это такой гигантский глобальный this) в целом это даже может работать, пока кто-нибудь не начнет мутировать данные в этом контексте из разных частей кода. То есть IoC через параметры метода конечно избавляет нас от одного вида зависимости, но оставляет другой, который вообще говоря тоже никому не нужен
                                                                                                            0

                                                                                                            Ну выведь понимаете что под IoC я имею в виду инверсию контроля, а не dependency injection или любой другой способ прокидывать параметры? DI это просто конкретный способ решать одну из задач соблюдения IoC в некотором пласте проблем.

                                                                                                  +2

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


                                                                                                  Примерно так же просто, как посмотреть на типы этих функций, действительно.

                                                                                                +6
                                                                                                Если вы этот код дописываете, то вы по идее видите что делает каждый метод.
                                                                                                То есть я правильно понимаю, что «для упрощения» мне предлагается разбить огромную функцию на 500 строк на 200 функций по 5-10 строк и потом, когда я хочу вот это вот править, я должен изучать уже не 500 строк, а 1500 строк?

                                                                                                Это точно называется «упрощение»? По моему это карго-культ называется.

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

                                                                                                Ибо эта вот «лапша» — это явное создание чего-то, что проще, чем это возможно. Если метод в 50, 100 или даже 500 строк не удаётся разбить на два метода, которые могут читаться и правиться совершенно независимо друг от друга… то его не нужно разбивать вообще!

                                                                                                Хотя в последнем случае, когда речь идёт о 500 строк, обычно удаётся выделить самостоятельные компоненты… но это не делается созданием десяти методов do_⅕_of_work, do_⅖_of_work и так далее.
                                                                                                  0
                                                                                                  Если вы просто разобьёте на функции определённого размера, то это будет карго-культ и читаемость не повысится. Разбить огромную функцию на более мелкие функции это как бы необходимое условие, но при этом даже близко не достаточное.

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

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

                                                                                                    Потому что следующим действием за шинковкой кода в лапшу следует его обмазывание большим-большим количеством юниттестов.

                                                                                                    Ребяяяты! Если вам, для того, чтобы понять — правильно вы поняли, что делает код или нет недосточно самого этого кода… то вы утратили его понимание, извините.

                                                                                                    Оно теперь у вас сосредоточено в тестах, а не в голове разработчика.

                                                                                                    И да, так иногда приходится-таки поступать… но это ни разу не то, к чему стоит стремиться…

                                                                                                    Но на мой взгляд редко какую функцию на 500 строк нельзя разбить на отдельные функции влезающие на экран монитора так чтобы читаемость при этом не повысилась.
                                                                                                    Зависит от монитора. Некоторые функции в 100 строк уже сложно делить. Но функции в 2-3 строки — это почти всегда профанация. Они очень редко имеют смысл сами по себе, то есть это либо требование языка (скажем какая-нибудь функция operator+ — это почти всего несамостоятельные 2-3 строки кода), либо часть группы функций… а тогда и не нужно считать что длина этой функции — 2-3 строки.

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

                                                                                                    Скажем в Haskell функции обычно структурированы реально как 1-2-3 строки… но при этом то, что я уподобил бы аналогу функции в обычных языках — это группа тесно связанных между собой функций, которые не имеют документации (то если не предназначены для самостоятельного использования).
                                                                                                      0
                                                                                                      Ребяяяты! Если вам, для того, чтобы понять — правильно вы поняли, что делает код или нет недосточно самого этого кода… то вы утратили его понимание, извините.

                                                                                                      Оно теперь у вас сосредоточено в тестах, а не в голове разработчика.

                                                                                                      Это как раз правильно, если только не заниматься Job security.


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

                                                                                                        0
                                                                                                        Нет. Идея нифига непонятно. Ибо цель — не получить текст, который приятно читать, а текст, который легко менять!

                                                                                                        Цель получить и то и другое. И когда у вас вместо одной огромной функции несколько небольших, то и менять их проще. Грубо говоря если вы ваш метод на 500 строк разобьёте на 25 методов, то 20 строк, то какова вероятность что вам придётся фиксить все 25 методов при каком-то минорном багфиксе? По моему опыту она стрeмится к нулю. Обычно придётся пофиксить 1-2 метода. Ну может 3-4. Ну максимум половину.

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

                                                                                                        Потому что следующим действием за шинковкой кода в лапшу следует его обмазывание большим-большим количеством юниттестов.

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

                                                                                                        Зависит от монитора.

                                                                                                        Конечно зависит. Но где бы я не работал обычно у людей мониторы были более-менее одинаковые и именно с этим пунктом ни разу проблем не возникало.

                                                                                                        Некоторые функции в 100 строк уже сложно делить. Но функции в 2-3 строки — это почти всегда профанация.

                                                                                                        Не надо пытаться всё обязaтельно разбить на функции по 2-3 строки. Но если у вас есть кусок кода, который можно вынести в отдельную функцию на 2-3 строки и ваша основная функция непомерно разрослась, то почему бы и не вынести?

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

                                                                                                        Естественно. И я пока ни разу не встречал «неделимый блок кода» длинной в 500 строк. И даже длинной в 100 строк не могу припомнить.
                                                                                              0

                                                                                              А как вы это будете делать в ФП? Особенно интересует обращение к isSuite :-)

                                                                                                0

                                                                                                MonadReader Bool m ?

                                                                                                  0

                                                                                                  Вот такого-то решения я и боялся.

                                                                                            0
                                                                                            С точки зрения ооп если вам интересен не весь контекст а только часть, у вас проблемы с cohesion) Впрочем этот аргумент работает в основном в идеальном мире
                                                                                        0
                                                                                        Методы в ООП взаимодействуют с состоянием объекта. Когда методы перестают это делать, а состояние начинает проталкиваться через аргументы, то код превращается в обычное процедурное программирование. Разве не так?

                                                                                        Так, только Мартин работает не с состоянием, а со скрытыми аргументами. Допустим, я меняю класс PrimeGenerator и вызываю checkOddNumbersForSubsequentPrimes. И тут у меня всё падает, потому что, оказывается, я должен был проинициализировать (статические!) поля primes и multiplesOfPrimeFactors, причём проинициализировать их в два этапа: присвоить каждому новый экземпляр соответствующего класса и вызвать set2AsFirstPrime. Если бы я написал точно такой код в процедурном языке, например, на C, то коллеги быстро объяснили бы мне, что это полнейшее безобразие. Но если перед этим безобразием написать слово class, то оно магически превращается в "чистый код" Мартина.


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


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

                                                                                          +1
                                                                                          . И тут у меня всё падает, потому что, оказывается, я должен был проинициализировать (статические!) поля primes и multiplesOfPrimeFactors,

                                                                                          Ну так сделайте не статический класс у которого все конструкторы требуют этих аргументов. В чём проблема то?

                                                                                          Если бы я написал точно такой код в процедурном языке, например, на C, то коллеги быстро объяснили бы мне, что это полнейшее безобразие.

                                                                                          Вам и в ООП языке коллеги очень быстро объяснят что так делать ну совсем не надо. Хотя это конечно в обоих случаях от коллег зависит.
                                                                                            0
                                                                                            Ну так сделайте не статический класс у которого все конструкторы требуют этих аргументов. В чём проблема то?

                                                                                            Я-то сделаю, проблема в том, что это приводится как пример "чистого кода" в популярной книге для новичков.

                                                                                              0
                                                                                              Я «Чистый код» читал достаточно давно. Но насколько я помню там обычно были не примеры «идального чистого кода», а примеры «как было» и «что стало если улучшить какой-то определённый аспект».
                                                                                        –12

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

                                                                                          +8
                                                                                          Что то вы совсем не в ту степь. Начать хотя бы с того что речь о книге дядюшки Боба, он же Роберт Мартин. А не о книге Фаулера.
                                                                                            0
                                                                                            Вот как раз хотел про Фаулера вставить, раз автор просит порекомендовать годноты.
                                                                                            Р.Мартина не читал, но осуждаю (после всего вышепрочитанного)!
                                                                                              +4
                                                                                              Ну по мне у дядюшки боба хорошего очень немало. Та же «Чистая архитектура» — там я вообще много на что кивал как болванчик соглашаясь. А «Чистый код», такое ощущение, писался будто он хочет довести каждую здравую идею до максимума, временами скатываясь в абсурд, непонятно зачем. Если подходить со здравым смыслом — я помню и в «Чистый код» немало полезного нашел.
                                                                                              А что касается книг, до Фаулера пока не дошел, но Макконел с его «Совершенный код» мне зашел отлично, на порядки лучше чем «Чистый код».
                                                                                                +4
                                                                                                Я так решил вернуться к истоком, читаю «Чистая архитектура» и во многом не согласен. Я попробовал посмотреть его выступления на конференциях, и у меня возникло ощущение, что старик выжил из ума. При всём уважении, принципы SOLID, TDD сейчас мне кажутся понятными намного меньше, чем 10 лет назад, когда я начинал работать. И я не могу понять — то ли я постарел, то ли индустрия изменилась, или же я сломался
                                                                                                  0
                                                                                                  При всём уважении, принципы SOLID, TDD сейчас мне кажутся понятными намного меньше, чем 10 лет назад, когда я начинал работать.

                                                                                                  Это нормально. У меня тоже чем больше опыт работы, тем больше вопросов возникает.

                                                                                              –1
                                                                                              Ну сути это не меняет. К чему тут эта истерика автора про то что кто-то не так функции назвал, аппеляции к каким-то устоявшимся определениям, которых автор сам не приводит (ну да, для нас важно его буквоедство). А финальная часть про то как у них на работе была церковь Чистого Кода, где они эту книгу как библию читал раз в неделю. Лол, ну вы сами виноваты, что решили что можно свои мозги заменить чужими.
                                                                                              Все эти книги пишуться для ознакомления, и совершенно не важно их читать а потом заниматься миссионерством и поисками серебрянной пули.
                                                                                              А вот уметь понимать чужой код и спокойно объяснять ошибки (а если бы автор так сделал, то осталось бы только короткое замечание про isSuite, который действительно бы следовало передавать как параметр)
                                                                                              Так что в основном книга походу хорошая, а все прдхявы автора чистая вкусовшина.

                                                                                              А, это еще и перевод, тогда понятно, хэштега #blm не хватает только)
                                                                                                +4
                                                                                                Ну автор может палку местами и перегнул, но доля правды в его словах есть ибо в книге примеры многие чисто для демонстрации конкретного принципа часто и потому доводятся до максимума. Показывая один принцип автор может на другие забить. Примеры все же синтетические нередко. Если подходить к книге с умом — книга хорошая. Если воспринимать буквально каждый совет (а новички так склонны делать) — книга действительно очень плоха как по мне.
                                                                                            +28
                                                                                            Ну да, ну да… Пошёл он нафиг, Чистый код...


                                                                                            Почему автор не привёл свой вариант рефакторинга всех этих классов?

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

                                                                                            Примеры в статье вырваны из контекста. Например SetupTeardownIncluder — это не идеал. Где Мартин сказал, что это супер классный кусок кода? Это результат рефакторинга, через разбиение толстых методов на маленькие. Который приводится, чтобы объяснить, что маленькие функции — это очень важно. Даже если не применять другие принципы из книги вовсе.

                                                                                            И вот мне интересно, какой процент из голосовавших читал эту книжку, и может сказать, что она являлась бесполезной тратой времени?

                                                                                            Что вообще вы ожидаете от книги? Волшебный секрет «Тайны драконы», который прибавит 200К к ЗП?

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

                                                                                            (Рискую кармой, а не головой.)
                                                                                              +1
                                                                                              Почему автор не привёл свой вариант рефакторинга всех этих классов?

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

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

                                                                                              Хотя теперь непонятно зачем вообще такой пример был нужен.
                                                                                                +3

                                                                                                Согласен. Чистый код и многие другие публикации Мартина отличное чтиво! Читал и перечитывал несколько раз.

                                                                                                  +2
                                                                                                  Почему автор не привёл свой вариант рефакторинга всех этих классов?

                                                                                                  Ну на мой взгляд стоило бы:


                                                                                                  1. убрать все приватные члены класса: по их использованию понятно, что тут нужна просто функция
                                                                                                  2. заинлайнить все приватные функции кроме includeSetupAndTeardownPages и тех что используются в ней напрямую
                                                                                                  3. а ещё лучше было бы вместо void Render() сделать RenderResult Render(), где Render — чистая функция, которая просто возвращает RenderResult, и второй компонент который будет превращать RenderResult в действие с минимальным количеством усилий. Потому что тут как раз нурашется SRP: класс одновременно решает и какие данные отрисовывтаь, и как. Разбить это на два этапа: формирование данных которые отрисовываем и рендерер, в котором нет никакой логики сложнее "отрисовываю то что мне дают" — было бы куда лучше, нет?

                                                                                                  Про PrimeGenerator молчу — код ужасен. Хороший мысленный эксперимент, который это показывает — а как оно будет работать в многопоточной среде? Правильно, всё развалится, причем нет никаких причин, почему нужно инициализировать primes/multiplesOfPrimeFactors. Лишнее действие, которое только ухудшает код.




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


                                                                                                  class PrimeGenerator {
                                                                                                    private final int[] primes;
                                                                                                    private final ArrayList<Integer> multiplesOfPrimeFactors;
                                                                                                  
                                                                                                    private PrimeGenerator(int n) {
                                                                                                      primes = new int[n];
                                                                                                      multiplesOfPrimeFactors = new ArrayList<Integer>();
                                                                                                    }
                                                                                                  
                                                                                                    protected static int[] generate(int n) {
                                                                                                      var generator = new PrimeGenerator(n);
                                                                                                      generator.set2AsFirstPrime();
                                                                                                      generator.checkOddNumbersForSubsequentPrimes();
                                                                                                      return generator.primes;
                                                                                                    }
                                                                                                  
                                                                                                    private void set2AsFirstPrime() {
                                                                                                      primes[0] = 2;
                                                                                                      multiplesOfPrimeFactors.add(2);
                                                                                                    }
                                                                                                  
                                                                                                    private void checkOddNumbersForSubsequentPrimes() {
                                                                                                      int primeIndex = 1;
                                                                                                      for (int candidate = 3;
                                                                                                           primeIndex < primes.length;
                                                                                                           candidate += 2) {
                                                                                                        if (isPrime(candidate))
                                                                                                          primes[primeIndex++] = candidate;
                                                                                                      }
                                                                                                    }
                                                                                                  
                                                                                                    private boolean isPrime(int candidate) {
                                                                                                      if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
                                                                                                        multiplesOfPrimeFactors.add(candidate);
                                                                                                        return false;
                                                                                                      }
                                                                                                      return isNotMultipleOfAnyPreviousPrimeFactor(candid