Лучшие практики, эмпирический опыт и математика

Original author: Ike Ku
  • Translation


Есть довольно простая идея, высказанная Фейнманом — цель физики найти простейшую теорию, которая сможет объяснить как можно больше явлений природы. Эта та идея, которая стоит за электродинамикой Максвелла или КЭД. Каждая новая большая теория объясняла больше явления природы и при этом была проще предшественников.


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


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


Категории


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


Теория категорий это про коллекцию объектов связанных стрелочками.

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


image

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


Второе то, что нам нужно для нашего рудиментарного анализа — это композиция.


Композиция


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


image

Если у нас есть функции length и isOdd между String, Number, и Boolean, то мы можем их связать эти объекты новой функцией isOdd ◦ length. Это, в общем, почти все, ну разве что есть пара простых правил.


length ◦ isOdd != isOdd ◦ length
length ◦ toLower = length

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


Keep it simple, stupid


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




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


image
f: A -> B
g: A -> C
k: A -> E
h: C -> D

В этой маленькой программе у нас есть одна точка входа (A) и возможность пройтись от нее к другим с помощью f, g, h, и k. Но это не все, мы можем даже сделать лучше и связать A и D напрямую композируя h и g.


image

h ◦ g: A -> D

В каком-то смысле, мы получили h ◦ g за так. И теперь можем отобразить A к D когда и если необходимость появится. Скажем в следующем спринте, когда менеджер продукта захочет показать инвойсы (D) пользователя (A) по транзакциям (C).


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


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


image
f: A -> B
m: B -> C
l: C -> E
h: C -> D

Мы заменили g и k из прошлого примера на m и l и при первом приближении потеряли отображение от A к C и A к E соответсвенно. Но у нас же есть композиция.


image

То же число изначальных функций на тех же типах дает нам аж 5 (!) новых функций. Это на 4 больше чем, что у нас было в прошлый раз.


m ◦ f: A -> C
h ◦ m: B -> E
l ◦ m: B -> D
l ◦ (m ◦ f): A -> E
h ◦ (m ◦ f): A -> D

Более того, мы получили наши g и k обратно через композицию. g стала m ◦ f, а k стала l ◦ (m ◦ f).




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


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

Это определение интересно не только тем что говорим нам о том что просто в KISS, но и говорит что простота относительна для каждого заданного приложения. Что это значит на самом деле? Если вы можете написать функцию a такую что через композицию с другими функциями в заданном приложении мы можем получить, скажем, 10 новых функции. Или, вы можете написать две функции b и c, которые через композицию могут дать, скажем, 20 новых. То, в общем случае, две функции b и c проще одной a.


Что дальше?


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


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

Similar posts

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

More

Comments 20

    +3

    Мне кажется, вы очень быстро проскочили через 'S-вопрос' (что такое Simple?). Я бы это прям назвал отдельной проблемой для software engineering, проблемой поиска определения простоты.


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


    1. Человек, который читает код, тратит пренебрежимо мало времени на понимание происходящего.
    2. Каждый объект в лексере (переменная, модуль, путь к файлу, класс, etc) имеет имя (или структуру, если это файлы), которая не вызывает вопросов "зачем". Чаще всего это best practices. Например, файл конфигурации для foo в /etc/foo.conf или в /etc/foo.conf.d/01-base.conf оба не вызовут вопросов, а вот /opt/foo/lib/shared/01-base.conf — вызовет и много.
    3. Каждый компонент придерживается одного из двух принципов: либо полная изоляция (чёрный ящик, на вход параметры, на выход результат и никому не интересно что внутри происходит), либо полная прозрачность (код ящика тривиален и выполняет очевидные действия над данными). Добавленной ценности мало, добавленной головой боли нет. Худший вариант — когда модуль пытается реализовать абстракцию, но делает её не до конца; в этой ситуации добавленной ценности маловато, а головной боли больше, чем ценности.
    4. Постоянное слежение за provenance любых артефактов и кода. Откуда этот файл? Чужой? Взятый из другого проекта? Как он обновляется? Как узнать откуда он был взят спустя 5 лет, когда оба проекта давно эволюционировали? Почему этот коммит, а не другой? Это был head или какой-то бранч?

    Т.е. моё определение S очень сильно завязано на объём onboarding и investigation со стороны человека, который имеет квалификацию, но не знаком с проектом до момента, когда этот человек может начинать осмысленно работать с кодом.

      +1

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

        0

        easy — это для глагола (пусть и подразумеваемого), а simple — для существительного.

      +2
      С одной стороны хорошо, что статья лёгкая для восприятия (а не как всегда в случае теории категорий).

      С другой стороны хочется спросить: «и что?». Какие выводы мы можем сделать из такой интерпретации KISS?

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

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

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

        +2
        Каждая новая большая теория объясняла больше явления природы и при этом была проще предшественников.

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


        В каком-то смысле, мы получили h ◦ g за так. И теперь можем отобразить A к D когда и если необходимость появится. Скажем в следующем спринте, когда менеджер продукта захочет показать инвойсы (D) пользователя (A) по транзакциям ©.

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

          +3
          Собственно, в отрыве от реальности ФП просто как 2 копейки, но в жизни обычно плохо получается так красиво всё формализировать
          IO код в Хаскелле мало чем отличается от обычного императивного языка. Так что формализуется всё запросто.

          и проект с обильным ФП очень часто становится write-only через пару месяцев.
          Спорное утверждение. При прочих равных, скорее проект без ФП станет write-only.
            0
            IO код в Хаскелле мало чем отличается от обычного императивного языка. Так что формализуется всё запросто.

            IO — да. Другие монады сложнее.


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

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


            const res = filter(isCourseItem, map(pipe(prop<any[]>('path'), p => slice(0, p.length - 3, p), when(<any>length, flip(path)(newState))), df1));

            Было бы написано с циклами — разобраться было бы быстрее.

              +1

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

                0
                IO — да. Другие монады сложнее.
                Те, что более-менее часто используются достаточно простые: Reader, Maybe, State. Всякие Cont и трансформеры уже действительно посложнее, но ими и пользуются сильно реже.
                Было бы написано с циклами — разобраться было бы быстрее.
                И код был бы гораздо длиннее. Не знаю, что это за язык, но примерно видно, что происходит фильтрация и обрезание массива. В цикле читать и понимать пришлось бы дольше. На Хаскелле без этой кучи скобок и указаний типов было бы короче и понятнее.
              +1
              Это если функции чистые.

              Это не единственная, и даже не главная проблема. Это все работает тогда, и только тогда, когда существует единственное отображение g: A → C и единственное отображение h: C → D.


              В противном случае отображение A → D ниоткуда не выводится, точнее их получается много, и без дополнительных оговорок неясно, что вообще такое h ◦ g. Для примера со строками, самого первого: есть isOdd: Number → Boolean, есть isEven: Number → Boolean. Ну и как теперь получить String → Boolean? Да никак. Проще и понятнее будет определить свою семантически верную функцию, для своего бизнес-случая. Именно поэтому хаскель так чудовищно многословен и перенасыщен таким количеством функций, делающих примерно одно и то же.




              проект с обильным ФП очень часто становится write-only через пару месяцев

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

              0
              И это именно то, как можно определить простоту.


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

              Нельзя вычислить, не беда, сравнивать то сложности мы можем, во всяком случае наши мозги как-то с этим справляются и мы можем сказать, что А сложнее Б. Можно даже выделить специального абстрактного Петю, который и будет единственный определять, является ли А сложнее Б. В такой ситуации хотелось бы уметь немного менять А и сравнивать результат с Б. Что-то вроде A + dx < Б? Для этого А нужно уметь представлять в дифференциальной форме. Тут затык, начинается такая математика, которую я не понимаю. Есть такая штука как differential lambda calculus, потенциально оно про это, но я не осилил.
                +1

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


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

                +2
                image
                Как это на практике работает? Например:
                A=string, B=boolean, C=int
                f=isEmpty A->B
                g=length A->C

                И как вы собираетесь строить m B->C?

                ps: это просто?
                image
                  0
                  B->C нельзя построить из A->C и A->B. Придётся писать новую функцию.
                  Точно так же, как и для каждой новой трансформации универсального объекта в вашем примере.

                  +2
                  В общем случае, нас даже не интересует как много функций есть между объектами — 1, 2, или бесконечность.

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


                  Да и вообще, универсальные свойства (а даже сам факт того, что если A — тип, и B — тип, то A → B тоже тип, с ними тесно связан) — они про то, что в некоторых особых диаграммах некоторые стрелки существуют и единственны (то есть, их ровно одна штука).

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

                    Ну чтож в другой раз.
                      +1
                      Есть довольно простая идея, высказанная Фейнманом — цель физики найти простейшую теорию, которая сможет объяснить как можно больше явлений природы. Эта та идея, которая стоит за электродинамикой Максвелла или КЭД. Каждая новая большая теория объясняла больше явления природы и при этом была проще предшественников.

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

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

                      Авиация по большей части живёт на накопленом опыте.
                        0
                        Простота кончилась на Ньютоне и Максвелле где-то.

                        Полагаю в 17 веке дифференциальное исчисление Ньютона казалось не меньшей абракадаброй чем КЭД сегодня. Если же вы можете понять уравнения Максвелла, то вы уже знаете, что такое уравнения в частных производных, пространственное дифференцирование, операторная запись. Со всеми этими знаниями до понимания КЕД не так уж и далеко.


                        Простоты, о которой вы говорите никогда не было, просто за 300 лет все привыкли к Ньютону. Через 300 лет привыкнут и к КЕД.

                        +1
                        Лично я очень жду продолжения. Интересно, куда дальше разовьётся мысль. Такими маленькими порциям читать — идеально, есть возможность аккуратно осмыслить и обдумать. Спасибо.

                        Only users with full accounts can post comments. Log in, please.