Pull to refresh

Comments 440

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

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

А действительно ли быстрее?
Если не рассматривать вебсервис формата «отдай html страницу», а хоть какую-то бизнес-логику?

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

Возможно я просто не умею писать на динамических ЯП, но у меня ни разу не получалось сделать так, чтобы то, что я пишу — заработало без ошибок. Если это не print(1+10).
Создать маленький скрипт, строк на 20, чтобы нейросеточку обучить или там данные в R обработать и график вывести — это ок. Написать, ну хотя-бы нормальный калькулятор, так чтобы новые функции туда вводить парой строчек кода — уже мучения с отладкой и вопросы небу «Что я сделал не так»?

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

PS
А большая часть ошибок возникает там, где были приняты решения формата: «Нет, мы не будем создавать тип для этого перечисления, мы запихаем сюда Int и будем проверять на этапе исполнения».

Я как-то писал код на солидити, и писал там связный список индексов массива (у меня был массив, а сбоку мне нужно было хранить порядок обхода элементов чтобы они были отсортированны по одному из полей). То есть у меня был head, tail, индекс 0+ обозначал индекс в массиве, индекс -1 — то что элемент не имеет следующего или предыдущего (в зависимости того в next или prev оно встретилось).


Так вот в какой-то момент я словил баг, который происходил когда по крайней мере 5 элементов вставлялись в определенном порядке (на 4 уже в любых их комбинациях не получалось воспроизвести). В какой-то момент список зацикливался. То есть вместо обхода 0 2 4 1 3 получалось что-то вроде 0 2 4 0 2 4 0 2 4 0 2 4 ...


Я достаточно быстро понял, что я где-то забываю писать -1 для next или prev. И я два дня дебажил 20 строчек кода, но так и не нашел в чем проблема. Для желающих, можете сами поискать баг, в коде:


Node[] nodes;
uint64 headIndex;
uint64 tailIndex;

function fixPlacementInHistory(uint64 newlyInsertedIndex, uint128 dateRegNumPair) private onlyOwner {
    if (newlyInsertedIndex == 0) {
        nodes[0].prev = -1;
        nodes[0].next = -1;
        return;
    }

    int index = tailIndex;
    while (index >= 0) {
        Node storage n = nodes[uint64(index)];
        if (n.request.dateRegNumPair <= dateRegNumPair) {
            break;
        }
        index = n.prev;
    }

    if (index < 0) {
        nodes[headIndex].prev = newlyInsertedIndex;
        nodes[newlyInsertedIndex].next = headIndex;
        nodes[newlyInsertedIndex].prev = -1;
        headIndex = newlyInsertedIndex;
    }
    else {
        Node storage node = nodes[uint64(index)];
        Node storage newNode = nodes[newlyInsertedIndex];
        newNode.prev = index;
        newNode.next = node.next;
        if (node.next > 0) {
            nodes[uint64(node.next)].prev = newlyInsertedIndex;
        } else {
            tailIndex = newlyInsertedIndex;
        }
        node.next = newlyInsertedIndex;
    }
}

Промучился я эти два дня… А потом за полчаса зачинил. Как? А вот так:


function fixPlacementInHistory(uint64 newlyInsertedIndex, uint128 dateRegNumPair) private onlyOwner {
    if (newlyInsertedIndex == 0) {
        return;
    }

    Types.OptionU64 memory currentIndex = Types.OptionU64(true, tailIndex);
    while (currentIndex.hasValue) {
        Node storage n = nodes[currentIndex.value];
        if (n.request.dateRegNumPair <= dateRegNumPair) {
            break;
        }
        currentIndex = n.prev;
    }

    if (!currentIndex.hasValue) {
        nodes[headIndex].prev = Types.OptionU64(true, newlyInsertedIndex);
        nodes[newlyInsertedIndex].next = Types.OptionU64(true, headIndex);
        headIndex = newlyInsertedIndex;
    }
    else {
        Node storage currentNode = nodes[currentIndex.value];
        Node storage newNode = nodes[newlyInsertedIndex];
        newNode.prev = currentIndex;
        newNode.next = currentNode.next;
        if (currentNode.next.hasValue) {
            nodes[currentNode.next.value].prev = Types.OptionU64(true, newlyInsertedIndex);
        } else if (currentIndex.value == tailIndex) {
            tailIndex = newlyInsertedIndex;
        }
        currentNode.next = Types.OptionU64(true, newlyInsertedIndex);
    }
}

library Types {
    struct OptionU64 {
        bool hasValue;
        uint64 value;
    }
}

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

Ну, я код ваш не отлаживал, потому за причину ошибки точно сказать не могу.
Но мне сразу бросилось в глаза некорректное сравнение в последнем if-then-else:
if (node.next > 0) {
0 — это вполне допустимое значение для ссылки на следующий элемент в цепочке, а код написан так, как будто 0 — пустое значение. Каковы последствия этой ошибки — я не анализировал, потому сказать, та ли это ошибка была или нет (и не было ли других), я не могу.
Естественно, использование явной проверки на непустое значение с помощью типчиков исправило эту ошибку. Но букв для этого вам написать пришлось заметно больше, не правда ли?
PS Однако экономия 2-х дней и потраченных за это время нервов лично для вас очевидно была целесообразной, не спорю.
Но мне сразу бросилось в глаза некорректное сравнение в последнем if-then-else:
if (node.next > 0) {

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


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

Ну, я не сторонник языков вроде J, поэтому для меня currentIndex.hasValue намного понятнее, чем currentIndex >= 0. Ну и да, нужно понимать, что солидити это совершенно отвратительный язык без средств нормального выражения, в нормальном языке разница была бы гораздо меньше.

На всякий случай спрошу, больше 1 раза newlyInsertedIndex нулевым быть не мог точно? Т.е. удалить, потом вставить опять — такого не было? И _не_ самым первым 0 индекс быть не мог тоже (хотя зачем в таком случае headindex)?

Потому что если такое могло быть — ошибка в обработке 0 индекса в самом начале. В коде с типами её нет.

Нет, массив работал только на вставку.

Она всегда начиналась с 0 индекса (зачем тогда headindex вообще)? Т.е. условие в самом начале всегда выполнялось, и могло быть выполнено только один раз (первый)?

Потому что head это не первый индекс, а индекс элемента с минимальным значением поля, по которому мы сортируем (в нашем случае, это поле dateRegNumPair). Если вы вставляем в такой последовательности


insert ("Hello", DateTime.Now)
insert ("World", DateTime.Now.AddMinutes(-10))

то головой будет 1.

Тогда вот он и баг, так можно только если всегда 0 индекс вставлять первым:
if (newlyInsertedIndex == 0) {
nodes[0].prev = -1;
nodes[0].next = -1;
return;
}


Кстати, второй вариант вообще не позволяет вставить в список элемент с индексом 0, если я правильно понимаю, т.к. просто сразу выбрасывается return-ом.

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

обычно

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

Видимо я плохо объяснил.


Массив всегда растет от нуля. То есть индексы всегда 0, 1, 2 и так далее.


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


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


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


В чем вы тут видите противоречие? Можно пример, который всё поломает?

Массив всегда растет от нуля.
Если так, то кроме >= вместо >, о которых уже написано в соседнем комментарии проблем не вижу.

Проблемы будут, если массив не всегда растёт от нуля (для меня было это не очевидно).

Вот и я так и не придумал, в чем проблема.


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

Вы смешиваете для индексов знаковые и незнаковые типы uint64 и int. Как они ведут себя при присваиваниях, при сравнениях в конкретной реализации языка? Delphi бы вам такого не простила, пришлось бы везде принудительно писать приведения типов (что есть плохо). Если для индекса достаточно диапазона 0..2147483647, и -1 для обозначения отсутствия ссылки, то лучше везде использовать int.

Где я смешиваю? Везде где я смешиваю я пишу явный каст, например uint64(index), причем только после проверки что число неотрицательное.


Если для индекса достаточно диапазона 0..2147483647, и -1 для обозначения отсутствия ссылки, то лучше везде использовать int.

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

Да, вот явный каст и есть плохо: получается, мы не используем типы в типизированном языке в полной мере. В Delphi можно написать например вот так:
TIndex = -1..2147483647;

И программа будет перепроверять значение индекса на выход за границы при каждом присвоении, при включении соотв. опции компиляции, как в Runtime (Range Check Error), так по возможности и в Compile time.
Ошибка у вас была как минимум здесь:
if (node.next > 0)

Вот реализация на Delphi (not tested):
Spoiler header

type
  TIndex = -1..2147483647;
  PNode = ^TNode;
  TNode = record
    Prev, Next: TIndex;
    Request: record
      DateRegNumPair: TDateTime;
    end;
  end;

var
  Nodes: array of TNode;
  HeadIndex, TailIndex: TIndex;

procedure FixPlacementInHistory(NewlyInsertedIndex: TIndex; DateRegNumPair: TDateTime);
var
  index: TIndex;
  node, newNode: PNode;
begin
  node := nil;
  index := TailIndex;
  while index >= 0 do begin
    node := @Nodes[index];
    if node.Request.DateRegNumPair <= DateRegNumPair then
      Break;
    index := node.prev
  end;
  newNode := @Nodes[NewlyInsertedIndex];
  newNode.Prev := index;
  if index < 0 then begin
    if HeadIndex >= 0 then
      Nodes[HeadIndex].prev := NewlyInsertedIndex
    else if TailIndex < 0 then
      TailIndex := NewlyInsertedIndex;
    Nodes[NewlyInsertedIndex].next := HeadIndex;
    HeadIndex := NewlyInsertedIndex
  end else begin
    newNode.next := node.next;
    if node.next < 0 then
      TailIndex := NewlyInsertedIndex
    else
      Nodes[node.next].prev := NewlyInsertedIndex;
    node.next := NewlyInsertedIndex
  end
end;

Есть еще преимущества например типизированной джавы vs джаваскрипт. Допутим у нас очень простой веб-сервис, мы на джаваскрипте анписали логику за час, на джаве-за полтора.
Но после создания веб-сервиса на спринге у меня сваггер будет сгенерирован автоматически из указанных мной типов, валидация входящих параметров также появится сама собой.
На джаваскрипте мне придется или добавлять те же типы и из них генерить сваггер или руками самому держать сваггер в акутальном состоянии. Валидация делается обычно также отдельно, например используется joi, для которого по сути я указываю те же типы еще раз, но в другом формате. И после этого мне нужны тесты чтобы быть уверенным что возвращаемые данные всегда соотвствуют выходному парметру-в джаве же в этом я буду уверен и так.
По факту у меня не выходит написание микросервсиов готовых к выходу на продакшен на джаваскрипте сильно быстрее. И это при том что в джаве все-таки не самая сильная система типов.
Статической типизации избегают либо трусливые параноики («я не знаю что поменяется завтра, надо сделать яп внутри яп нафигачить всё динамически»), либо люди, которые не могут в формализацию.
… или просто не любят писать слишком много лишнего кода.
Вывод типов позсоляет лишнего кода почти не писать, а полиморфизм писать кода даже меньше, чем в большенстве динамически типизированных языках (ну может кроме Julia и Common Lisp).
К тому же IDE часто позволяет генерировать код исходя из информации о типах.
Полиморфизм никак не связан со строгостью системы типов. И да, для динамически типизированных языков тоже придумали IDE с автогенерацией кода.
UFO just landed and posted this here
Для полиморфизма по параметризованным типам (например монадам) требуются параметризованные типы, из динамических языков я такие знаю только в Julia (в CL можно сделать, но я не видел и сам не пробовал).
Современные IDE пытаются изобразить статически типизированный язык из динамически типизированного, прогоняя вывод типов. Но информации для полноценной реализации Type-Driven Development у них просто нет.

Вот, кстати, да.


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


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

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

Можно было бы тогда взять просто статически типизированный язык, и тем самым воспользоваться уже имеющейся проверкой типов и преимуществами с Type-Driven Development.
Еще раз повторю (я уже устал, правда): просто взяв статический язык, вы полностью лишаете себя динамичности (об этом ниже), которая иногда очень выручает. Иногда нам нужно сначала быстро решить проблему, а уже потом решить хорошо, это реальность. Так вот динамичность позволяет творить невероятную магию.

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

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

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

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

У вас всегда есть что-то вроде базового «object», который вам позволит забить на статическую типизацию и вы можете делать вид что у вас типизация динамическая. Вот только зачем?
но вот на практике

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

Вот только зачем?

Мне второй раз написать, зачем?
Конечно, ведь не существует в мире крутых качественных проектов, где под капотом динамика

А этого я нигде и не утверждал. Но на мой взгляд такие варианты в итоге оказывались всё-таки затратнее.
Мне второй раз написать, зачем?

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

Хорошо, если в этот аргумент вы не верите, тогда мне нечего ответить.

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

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

Как Кларк написал в одном из своих законов: «Любая достаточно развитая технология неотличима от магии».

Что в общем-то означает что магия это технология, которая просто слишком развита для понимания тем, кто считает её магией. Так что как по мне то никакой магии нам в информатике не надо :)
UFO just landed and posted this here
UFO just landed and posted this here
Распарсить мегабайтный json или xml, поменять там пару строк и запарсить обратно. Сейчас у нас везде микросервисы в облаках, которые не должны знать друг о друге. А с точки зрения юзера это всё ещё один документ (файл). В котором вы знаете свой селектор (не путь).
И почему это по вашему мнению в статическом виде невозможно или прям таки сильно затратнее? Ну то есть в чём конкретно проблема должна заключаться?

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


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

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

Ну вообще-то даже для «полной статики» вам не нужно сериализировать абсолютно все входящие данные. Кто вам запрещает сделать это только для тех самых интересующих вас «пары строк»?

Отсутствие инструментов.


Я как-то начинал писать такой инструмент, но в итоге так и не закончил.

Ну в случае с XML и C# я делал что-то подoбное при помощи банального XmlReader и скажем LINQToXML.
Или я не совсем понимаю в чём конкретно проблематика…

Вот у вас есть функция:


void Foo(XElement foo)
{
    foo.Element("bar").SetElementValue("baz", 1);
}

Передайте ей вот такой документ и посмотрите как всё замечательно грохнется в рантайме:


<foo>
    <baz>2</baz>
</foo>

Фактически, это ослабление типизации.

Извините, а что у вас произойдёт в случае с динамической типизацией если вы запихнёте инвалидный XML? Либо тоже грохнется, либо поменяет вам там что-то не так и оно «грохнется» когда-то позже. Или если совсем «повезёт», то не грохнется, а что-то там неправильно выполнит и вы потом будете обьяснять клиенту почему у него что-то там пошло не так и он влетел на миллионы.

П.С. Ну и если я знаю что могу получить инвалидный XML, то я точно так же могу написать обработку исключений. В чём проблема то?

Пожалуйста, читайте мои сообщения внимательнее. Что за случай с динамической типизацией вы "у меня" нашли?


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

Я теперь вообще не понимаю о чём вы и к чему вы… И в чём в приведённом вами примере должна заключаться разница между динамической и статической типизацией в контексте задачи «поменять пару строк».

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

Ну так что мешает вам использовать XmlSerializer только на те куски хml, которые лично вас интересуют?

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

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

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

А где я говорил, что у статической типизации есть недостатки (кроме отсутствия идеальных инструментов)?

xml & set (el "bar" . el "baz" . text) "1"

В рантайме ничего не падает. Фактически в таком случае ничего не происходит.

В рантайме ничего не падает. Фактическо в таком случае ничего не происходит.

Это ещё хуже, чем когда падает. Программа работает некорректно, но об этом никто не знает...

Необходимая валидация просто делается заранее и всё:


let accessor = el "bar" . el "baz" . text
in if not $ null $ xml ^.. accessor
  then xml & set accessor "1"
  else error "invalid xml"

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

Так ведь задача состоит именно в том, чтобы этого не делать.

Кто вам такую задачу поставил? Я не вижу в формулировке "Распарсить мегабайтный json или xml, поменять там пару строк и запарсить обратно." никакого запрета на проверку корректности вашей программы.

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

UFO just landed and posted this here

Проблема не в Nothing, а в том, что программист вообще не должен был писать .Element("bar").

А как решить проблему если программист неправильно прочитал задачу в джире?

Передайте ей вот такой документ и посмотрите как всё замечательно грохнется в рантайме:

<foo>
    <baz>2</baz>
</foo>

Я представляю как что-то вроде такого:

class Foo {
  int baz;
}

var foo = xml.parse<Foo>(path)
foo.bar; // ошибка компиляции


Ну и да, xml, как динамический язык, может грохнуться при парсинге, но что ещё ждать от динамики?

Мы вот отказались от xml и пишем данные на C#, у нас такой проблемы нету.

Я писал про проблемы Linq2XML, а вы отвечаете про совсем другой механизм.

UFO just landed and posted this here

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

… дежавю. Только там говорилось про IO.

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

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

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


без каких-либо проверок

Как это без проверок? Там в сигнатурах вполне ясно записано, каким образом аксессор работает и что гарантирует: Lens, Traversal, Prism.


и оптимизаций

При этом оно всё ещё намного оптимальнее, чем в любом монотипизированном языке.


Поставленную задачу (найти по селектору) не решает

По какому селектору вы с этим инструментарием что-то не сможете найти, не поделитесь?


даже комментарии ни та ни другая блин не обрабатывают.

В XML обрабатывает, а в валидном JSON комментариев быть не может.

При этом оно всё ещё намного оптимальнее, чем в любом монотипизированном языке.

Пруфы какие-нибудь есть? Или как обычно у математков, п.в. («почти всегда»)?
В XML обрабатывает

Не вижу этой обработки. Пример: кусок невалидного XML с выбранным селектором, целиком заключенным в комментарий. Т.е.
<!--badXML <selector<a></b>/selector></badXML>-->
Можно пример таких задач?
Вы хотите примеров, не вникая в архитектуру? Все такие оторванные от контекста примеры на конях в вакууме выглядят глупо, никогда не убеждают собеседника и решаются элементарно.

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

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

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

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


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

Да, поэтому в нормальной библиотеке у вас будет тип аргумента NonZero<int>, и ноль передать туда вы не сможете.

в нормальной библиотеке у вас будет тип аргумента NonZero<int>, и ноль передать туда вы не сможете.

В буквоедство интереснее играть вдвоём. Теперь представьте, что у вас три числовых параметра a, b и c, и для них должны выполняться неравенства треугольника. Но только если Луна не находится в третьем Доме.
Дальше, предположим даже, что ваш язык позволяет накладывать подобные ограничения на типы. Много ли кто будет способен это корректно сделать? Много ли кто будет этим реально заморачиваться?
Наконец, главной претензией к динамической типизации называют стоимость дальнейшей поддержки. Хорошо, предположим вы выпустили библиотеку с NonZero<int>, она пошла в массы и обросла пользователями. Дальше выяснилось, что по новым веяниям законодательства/науки/бизнес-требований этот int таки может быть 0, просто нужно использовать чуть другую формулу. Но все кругом уже привыкли, что у вас NonZero<int>, хранят в своих структурах NonZero<int> и в куче мест делят на этот int, зная, что он точно NonZero. Короче, ваше, как оказалось, ошибочное требование расползлось по сотням кодовых баз. В языке с динамической типизацией вы бы просто поменяли реализацию своей функции. В статике вам нужно выпускать следующую версию апи, ломать обратную совместимость и заставлять всех медленно и мучительно обновляться.

Почему одно и то же изменение в динамике ломает API, а в статике — нет?

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

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

Вроде вся статья была про ваше заблуждение, но вы так и не увидели проблемы?


Защититься от изменения формата данных можно на любой типизации. Изменение интерфейса (читай типа) — ни в каком.


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


А вот вам обратный пример. Теперь вы обязаны передавать ноль. По закону. Иначе тюрьма. В статической типизации я изменю сигнатуру кода и все сразу получат ошибку компиляции и не сядут в тюрьму. А вот чуваки с динамической типизацией должны будут исследовать сотни тысяч строк кода в надежде, что теперь никто не передаёт "0". И молиться что ничего не забыли. Иначе всё, тюрьма)

В буквоедство интереснее играть вдвоём. Теперь представьте, что у вас три числовых параметра a, b и c, и для них должны выполняться неравенства треугольника. Но только если Луна не находится в третьем Доме.

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


foo :: (Int ** na, Int ** nb, Int ** nc) where abs(nc) <= abs(na) + abs(nb)

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


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

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

Вы не поняли мой предыдущий комментарий. Строгая типизация накладывает ограничения не только на библиотеки, но и на пользователей этих библиотек, даже если им эти ограничения нафиг не нужны. А люди имеют привычку немножечко лениться. В нашем примере, вместо того, чтобы каждый раз при вызове функции превращать int в NonZero<int>, они проверяют один раз и просто хранят NonZero<int> — хотя им, допустим, всё равно, ноль там или нет. При обновлении апи проблемы возникнут именно из-за этого.
В С++, например, если у NonZero есть конструктор, который принимает int, то никаких проблем не будет. Мне кажется, что эти «ограничения на юзеров» есть фича строгой типизации.
Проблем будет.
1. Даже если есть тривиальный конструктор, его кто-то должен формально вызвать. Линковщик о нём ничего не знает, так что минимум перекомпиляция.
2. Может не сработать, если этот конструктор explicit, или если нарушается правило «не более одного неявного преобразования». Например, изначальный тип — short, в int он будет преобразован, а вот в NonZero<int> уже нет.

Напишите шаблонный конструктор (и заодно преобразование обратно), для всех типов соответствующих трейту std::is_integral?)


Вы явно недооцениваете современные статически-типизированные языки в их возможности обобщать :)

Ага, а обратной совместимости никогда не существовало и не будет существовать.
UFO just landed and posted this here
Если нормально готовить динамические языки, ты узнаешь об этом в момент прогона тестов

Тесты защищают только тогда, когда они старательно пишутся и так-же старательно запускаются.
Хорошие тесты писать сложнее, чем хорошие типы. Да и прогон тестов обычно долше компиляции.
UFO just landed and posted this here
индуктивно гарантирует корректность всей программы

Это только в волшебном мире с единорогами и радугами.
В реальном мире будет Segmentation Fault / Unexpected type exception.

UFO just landed and posted this here

Unexpected type exception — это вот то самое из мира с динамической типизацией. Запихнули гавно и узнали об этом на продакшене :)

И unexpected type exception не получал (более того, даже не знаю, что это такое).

Это когда запускаешь код с -fdefer-type-errors

UFO just landed and posted this here
А можно уточнить, какое принципиальное отличие между спагетти-кодом на питоне и на го? Есть ли какой-то секретный закон, согласно которому 10 хороших программистов обязательно напишут плохой код на динамическом языке и 10 откровенно плохих программистов напишут идеально поддерживаемый код на статически типизированном?
Да дело даже не в хорошем или плохом коде. Со временем когда mvp или poc взлетел, код будет изменяться с огромной скорость. Через годик-два, если это не статический язык, добавление строчки кода будет опаснее коронавируса =)) И что бы каждый раз не какать в штаны при деплое, будут писаться тесты на самые элементарные вещи, но это не всегда будет помогать. Люди будут передавать инлайн обьекты, попутно добавляя и убирая фиелды… ты будешь смотреть на код и не понимать с какими структурами данных ты работаешь, потому что их нет. Люди будут делать волшебные вещи которые язык делать позволяет.

Рефактор? Это ад. Дай мне программу на го, удали часть когда, я его восстановлю и починю только глядя на ошибки компайлера.

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

з.ы.
Вне веба, имхо немного другие критерии. Например обработку текста и матриц делать на го или жаве, то еще удовольствие… а вот на пайтоне — рай для души.
Обработка матриц на пайтоне без внешних библиотек?
Что за искусственное ограничение? Если считать использование внешних библиотек минусом, то тогда лучший язык — тот, который всё в stdlib впихнул?

Просто тот же numpy по факту не на Python написан. И это добавляет головной боли с компиляцией нативного кода

Просто тот же numpy по факту не на Python написан.

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

И это добавляет головной боли с компиляцией нативного кода

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

Да ну?

Возможно неточно выразился — имел в виду, что новые языки не обходятся без хоть каких-то библиотек, написанных на старых; не обязательно про матричные вычисления.
Проблемы возникают при попытке запустить это в сколь-нибудь нестандартном окружении. То нужного компилятора нет, то .so/.dll не ищется, то случайно цепляется python из другого пакета.
Так всё равно для хорошей производительности матричных и т.п. вычислений нужен blas, который идёт отдельной библиотекой. Причём часто даже без исходников — MKL широко используется. Ну а если blas быстрый не нужен, то numpy на x86 и arm по крайней мере легко устанавливается, никогда с ним не было проблем (с другими, намного менее популярными библиотеками в питоне, бывали).
Во первых, управление библиотеками — не самое приятное занятие, когда собираешься быстро решить задачу. Тем более что работа с матрицами во многих языках уже сразу хорошо реализована.
Во вторых, идеоматические приемы работы с numpy заметно отличаются от ванильного питона. Да и вообще современные библиотеки (во всех языках) уже тянут на отдельный DSL и каждую изучать придется фактически как новый язык.
Не могу согласиться совсем.
Во первых, управление библиотеками — не самое приятное занятие, когда собираешься быстро решить задачу.

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

Тем более что работа с матрицами во многих языках уже сразу хорошо реализована.

В каких «многих»? Может быть в C, C++, C#, Java, Ruby, PHP удобная работа с матрицами? В питоне numpy является единственным используемым вариантом по сути, любая библиотека где нужны числовые массивы его поддерживает/использует.
Дай мне программу на го, удали часть когда, я его восстановлю и починю только глядя на ошибки компайлера.

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

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

Интересное наблюдение. Я на go никогда не писал, но это утверждение по сути означает, что типичный код на нём очень сильно избыточный, раз его часть можно без особых потерь восстановить?
На Elm писать однозначно быстрее, чем на жаваскрипте.

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


Я в С++ могу нафигачить хэш таблиц с void* указателями и кастить их к нужным типам только там где нужно. Просто это будет МЕДЛЕННО. Зато гибко. Вот собственно и вся история против динамической типизации и за статическую :)

UFO just landed and posted this here

Но если добавить ещё одно обязательное требование — безопасность (в слабом смысле, то есть когда объекты или другие сущности изолированы друг от друга), то уже от проверок в рантайме уже не уйти, почти каждый доступ к объекту сопровождается накладными расходами, разве не так?




Конечно, есть исследования по gradual typing, когда система построенная на динамических типах постепенно обрастает статическими типами, и это позволяет постепенно избавляться от рантайм-проверок, но у этого подхода тоже есть существенные недостатки (см исследования учёного по имени Matthias Felleisen).

UFO just landed and posted this here

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


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


1) Данные почти всегда неизвестного размера и неизвестной формы. Тяжело применить любые оптимизации.


2) Всегда нужно валидировать и валидировать много. Это постоянные if-ы, явные или не очень, при любом обращении к объекту. Сильно ломает уже оптимизации железные.


Этих двух пунктов достаточно, чтобы сделать код очень медленным :)


Вообще, пишите на С++ :) Там есть и статическая типизация и динамическая (шаблоны).

C++ мёртв, он обрастает костылями с невиданной скоростью, писать на нём в 2к20 отвратно.
Лучше уж Nim, D, Go, Rust.

C++ живее всех живых, к сожалению или к счастью решать уже не мне :) Я тоже топлю за Rust, но во-первых, этот язык позиционирует себя как более безопасная замена Си (не С++). Хотя конечно в целом, он имеет все шансы лет через 15 его заменить.


P.S.
На С++20, кстати, писать не так уж и отвратно. Плюсы отвратно учить, это да.

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


Совсем недавно встретился вот такой баг в современном C++ (который отчасти фича): https://wandbox.org/permlink/7sbsqzhbo0o7dOse

Не то, чтобы это был баг. Деструкторы для member-ов не вызываются в случае исключения в конструкторе еще с С++98. Именно поэтому все классы, принимающие лочки в себя, делают это всегда по ссылке.


Сказал бы я, что это "нормально", но нет, конечно это не так :)

UFO just landed and posted this here
> Деструкторы для member-ов не вызываются в случае исключения в конструкторе еще с С++98.

Вызываются, если соответствующие конструирования уже были завершены в конструкторе.
код
#include <iostream>

using namespace std;

struct V {
  int value;
  V(int v) : value(v) { cout << "V " << value << endl; }
  ~V() { cout << "~V " << value << endl; }
};

struct X {
  V v;
  X(int);
  ~X();
};

X::X(int a) : v(a) {
  cout << "X" << endl;
  if (a != 0) {
    throw 1;
  }
}

X::~X() {
  cout << "~X" << endl;
}

int main() {
  try {
    X x0(0);
    cout << "After x0" << endl;
    X x1(1);
    cout << "After x1" << endl;
  } catch (int& i) {
    cout << "Exception: " << i << endl;
  }
}




получаем вывод:
вывод
V 0
X
After x0
V 1
X
~V 1
~X
~V 0
Exception: 1



GCC 7, C++11.
Но если исключение не ловить, то деструкторы не вызовутся (оптимизация или намеренно? всё равно программа уже неживая).

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

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

Да, моя ошибка, затупил.

В примере по ссылке поведение зависит от компилятора. Если выбрать clang — работает как надо, деструктор вызывается.
Без HTK с бустом тяжело конкурировать.
Если выбрать clang, то работает как должно — деструктор срабатывает.
Вероятно, это баг в gcc

а можно подробнее об "отчасти фича"?

Я считаю это кумулятивным эффектом от следующих фич C++: исключения, в том числе и в конструкторе, неопределённого порядка передачи параметров при конструировании {}, правил вызова конструкторов/деструкторов во время исключений и плюс эффект какого-то очень нетривиального бага в gcc.
Во-всяком случае если этот код чуть-чуть подправлять по-разному, то баг исчезает. И вдобавок не воспроизводится в clang (но вероятно, там свои тоже очень нетривиальные).

Это вроде бы понятная фича языка. Если конструктор объекта не завершился, а был прерван exception'ом, то деструктор не позовётся. Как можно сделать иначе? При этом все деструкторы для уже сконструированных объектов будут вызваны. В чём тут проблема?

Дело в том, что в примере выше конструктор вызывается, а деструктор — нет. То есть создан такой сценарий использования lock_guard_ext, при котором идиома RAII развалилась, и это кмк весьма неприятно.

Там кажется компилятор слишком вольно реализуют aggregate initialization, если явно задать конструктор, то деструктор у lock'а зовётся там где ожидается. В целом да, выглядит как баг в gcc. AFAIK, в gcc про это (evaluation order и т.д.) достаточно багов было.
UFO just landed and posted this here
И это утверждение совместно с тезисом «статическая типизация для производительности не обязательна».

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


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

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


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

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


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

Ну, вот поздравляю, вы прикрутили типизацию :) Вместо явного описания типа, вы написали assert-ов :)

хм… а я разве этот тезис оспаривал?

Оспаривал-то утверждение "(помимо неопределённого размера) if внутри runtime-типизации ломает аппаратные оптимизации".

Нет не ломает (есть средства чтобы не ломались) хД
Если так говорить, то каждому своё и можно оооооочень долго спорить что лучше или хуже. Я всё-таки оставлю свой голос за статикой, но нельзя отрицать, что динамические решения сейчас (как и в принципе всегда с момента их появления) активно продвигаются и во всю юзаются. Даже если взять C#, например, то даже и там используешь ключевое слово dynamic и вот тебе динамизм без всякого псевдодинамизма в виде приведения типов)))
Даже если взять C#, например, то даже и там используешь ключевое слово dynamic и вот тебе динамизм без всякого псевдодинамизма в виде приведения типов)))

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

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

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

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

А как вы не продебажите обработчик не создавая его заранее?

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


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

UFO just landed and posted this here

Она отдает один тип — хэшмапу ключ-значение.

И действительно оказывается, что функции pickle.load() можно дать совершенно разумный тип в Java:

Я бы переформулировал это так: чтобы не писать на уже готовом динамическом языке, который фу-фу-фу, мы (в очередной раз) имплементируем его часть − динамическую систему типов − на нашем любимом статически типизируемом языке. Потому что Serializable − это же только интерфейс. В общем, NIH-синдром в полный рост.

Дженерик-функция не динамична. Nih тут вообще ни при чем.

Тогда зачем стопицотый проект на C, C++ или Java в стопицотый раз имплементирует динамическую систему типов вместо того, чтобы использовать по назначению уже имеющиеся в наличии Lua или там Groovy? По мне так явный NIH-синдром.
Тогда зачем

Очень часто, из-за неумения готовить. Разработчики приходят из Javascript фронтенда в бэк на Java и пытаются там воспроизвести привычный мир.
Вряд ли это основная причина. Скорее, наоборот, вполне себе опытные разработчики на статически типизированных ЯП не понимают, что такое динамические системы типов и зачем они нужны (некоторые вообще отрицают существование динамических типов). И поэтому вынуждены каждый раз переизобретать их.

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

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

А если вы хотите обоснований, можете познакомиться с проектом, в котором я долгое время варился. Он имеет весьма навороченную систему динамической типизации, маскирующуюся под сериализатор данных. Эта система порождает множество проблем, начиная с мелких багов и кончая принципиальной сложностью её более-менее интероперабильной, кросплатформенной реализации. А между тем разработчикам достаточно было встроить какой-нибудь динамический язык (благо, на JVM реализовано много универсальных ЯП: Python, Ruby, JavaScript, Lisp, Tcl), чтобы забыть о реестрах типов, версионировании объектов (Duck Typing), интеропе (те же ЯП встраиваются и в C++, и в C#) и много о чём ещё. Заодно и облегчить подключение кастомного кода при распределённых вычислениях. А критичные части системы (сеть, обнаружение нод, алгоритмы дупликации и восстановления данных) оставить как есть.

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

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

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

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

Автор вот говорит, что можно терпеть, и даже описывает какие-то best practices. Но я чаще слышу противоположное мнение. А лучшие практики ИМХО − это такие практики, которым проще следовать, чем не следовать. Когда их знаешь, конечно.

А причём тут JSON, если в Apache Ignite используется свой формат сообщений?

Я пытаюсь сказать, что динамические данные проще парсить динамическим языком.

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

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

Обратный процесс также существует — в Python и PHP добавили аннотации типов, для JS существуют Flow / Typescript. Но что это доказывает?

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

UFO just landed and posted this here
Когда вам говорят, что вы чего-то не знаете или не понимаете, а вы приравниваете это к обвинению в глупости − это в вас говорит снобизм или чувство превосходства.
Сначала вы как бы задали вопрос «Зачем?» (правда уже вопрос был сформулирован скорее не как вопрос, а как наезд). Затем вы услышали ответ, но вас он не устроил и вы решили дать свой ответ. Уже в этом месте очевидно, что вы пришли в эту ветку не конструктивно обсуждать, а поспорить и наехать (форма всех реплик только подтверждает это). Достойное желание и как следствие — достойная награда в виде минусов (кстати, я ещё и не минусовал :) )
Вы отчасти правы, у меня действительно «подгорает», когда мои любимые инструменты задвигают в угол. Но ответа на свой вопрос я всё-таки не получил. Допустим, го-свичеры написали cty, потому что заскучали по Пайтону, но как тогда насчёт GObject? А имплементациям этим нет числа, не зря появилось правило Гринспуна.

Го по сути является языком, который сделан для людей которые пишут на динамических языках, и сам не особо статический, любую нетривиальную логику надо писать как interface {} через interface {}, если конечно не хотите писать кучу кодгенов (которые еще запускать надо), а из средств выразительности только слайсы и хэшмапы.


Не могу не привести одну подходящую под это цитату из самой обсуждаемой статьи:


Although I can’t give it the full treatment it deserves right now, I’d still like to touch on the idea briefly so that interested readers may be able to find other resources on the subject should they wish to do so. The key idea is that many dynamically typed languages idiomatically reuse simple data structures like hashmaps to represent what in statically-typed languages are often represented by bespoke datatypes (usually defined as classes or structs).

These two styles facilitate very different flavors of programming. A JavaScript or Clojure program may represent a record as a hashmap from string or symbol keys to values, written using object or hash literals and manipulated using ordinary functions from the standard library that manipulate keys and values in a generic way. This makes it straightforward to take two records and union their fields or to take an arbitrary (or even dynamic) subselection of fields from an existing record.

In contrast, most static type systems do not allow such free-form manipulation of records because records are not maps at all but unique types distinct from all other types. These types are uniquely identified by their (fully-qualified) name, hence the term nominal typing. If you wish to take a subselection of a struct’s fields, you must define an entirely new struct; doing this often creates an explosion of awkward boilerplate.

Поэтому приводить в пример Go я бы не стал.

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

написали cty,

Ну вот я читаю описание cty и в первом абзаце вижу — «The primary intended use is for implementing configuration languages»
Аналогичная ситуация была в паре других проектов, где я варился — люди создавали отдельный язык для конфигурации приложения (то есть ядро системы было написана на одном языке, а для дополнительной конфигурации пользователем предлагался другой). Не знаю, как насчёт cty, но в двух мною наблюдаемых случаях это действительно давало возможность что-то быстренько «сконфигурировать», но помере роста требований к конфигурации и объёма, становилось неуправляемым. Разработчики ядра обычно в таких случаях делали вид, что их это не касается — «в ядре всё хорошо, а кастомизации это уже не наша забота».
:) Мой вариант красивей, но именно ваш, скорей всего, автор комментария имел в виду.
Мои полторы копейки.

Легаси. PHP. В одной из переменных класса есть член (тут можно ставить точку) AuthUser. Описания что это такое нет. Типа нет. Населена роботами.

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

Да, об этом статья и говорит: у AuthUser, с которым вам приходится работать, есть тип, и у этого типа есть определение, но оно (определение) раскидано кусочками по всему коду, который использует этот объект тем или иным способом, то есть неявно.

Я вот изредка натыкаюсь на истории про PHP подобные, и какой кайф что я его только пол годика назад начал применять. Нормальная типизация, уже даже в описании класса добавили возможость писать что типа имеет переменная. Я на нем пишу как на сишке, после питона как в раю.
Откровенно говоря, не спасает. Статическая типизация не дает другим накидать мне непонятного дерьма, а то что у меня под капотом фурычит я и так в принципе знаю.
Как так? Почему у вас в команде кто-то типизирует код, а кто-то нет. Такие вещи должны быть закреплены в style guide и проверяться линтером.
Далеко не всегда разработка идёт исключительно в рамках одной команды. А бывают ещё такие вещи как разные филиалы, аутсорс, сторонние библиотеки/решения и т.д. и т.п.
Безусловно все описанное в статье очевидно и трудно с этим поспорить. Но где бы найти такой язык с полной инфраструктурой кроссплатформенной (иде, дебаггер, профайлер) и с хорошим сообществом, у которого была бы сильная типизация и возможность легко абстрагироваться от не нужной информации и легко вводить нужную. Мой профессиональный язык кажется далек от этого. Например для индекса в массиве заставляет указывать количество битов, и не позволяет зашить в нем ограничение на выход из диапазона указанного масива.

Rust? Разве что IDE нет, но VS Code справляется неплохо и есть плагин для CLion, но им не пользовался, не подскажу как работает. Тулинг в целом какой-никакой есть. Есть лайфтаймы и от них больно, но в сущности, как и ловушка с динамической типизацией — если не писать лайфтаймы, это не значит, что их нет :) Всё равно придется о них думать.

Учитывая ваш текущий язык возможно вам подойдет Rust. Для обычных крудов подойдет Scala. Для эстетов — Haskell.

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

F# действительно слабоват, в основном это проблема сообщества, которому "нинужна" никаких фичей из более мощных систем типов. Знакомая риторика, нда.


Что до хаскелля на винде — у меня вполне успешно получилось написать прототипный проект на servant + servant-swagger + persistent. То есть по крайней мере ходить в постгрю и отдавать жсоны можно без проблем. Возможно глубже там какие-то проблемы, глубоко я не копал, но в первом приближении можно и так.


Во-втором приближении можно собирать в wsl или докере, это довольно популярно.


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


В общем, варианты есть.

F# действительно слабоват, в основном это проблема сообщества, которому "нинужна" никаких фичей из более мощных систем типов. Знакомая риторика, нда.

Возможно также это и ограничение IL, который проектировался под ООП языки: C# и Visual Basic. По крайней мере ограничение по производительности.


Ну и пробовал немного F# — не зашел, не нашел достаточной причины, чтобы перелезать с C#. Либо не распробовал, либо мне слишком нравится писать производительный код, который не напишешь в F#, а если и напишешь, то выглядеть он будет отвратно.

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

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


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

D? Юзаю с VS Code и плагином code-d. Есть для него еще Visual D (плагин для Visual Studio) и какие-то другие IDE.
Если кроссплатформенности Win — OSX — Linux — iOS — Android достаточно, то… (одевает бронежилет и защитный шлем)… Delphi. В Web тоже можно, с thirdparty библиотеками.
Кроссплатформенность — хорошо, но что там с зависимыми типами и другими ребрами лямбда куба? Есть большие сомнения. Но сам язык Pascal был в свое время не плох. Остались только хорошие воспоминания о нем со школы.
UFO just landed and posted this here
tl;dr. Так понял, речь идёт о сторонниках явного и неявного приведения типов. Лично для меня динамическая типизация проблемней тем, что требует дополнительных проверок типа перед проверкой значения. С другой стороны строгая типизация требует систематического явного преобразования типов, но по мне это небольшая жертва ради стабильности кода.
Номинативны ли классы в Пайтон? вот вопрос. Согласно определению номинативной структуры типов — да, номинативны, ибо два класса одной структуры, имея разное имя тождественными считаться не будут. Но при этом использование этих классов может быть в питоне абсолютно тождественным в силу динамической типизации.

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

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

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

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

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

P.S. Спасибо за статью. Отделение номинативной и структурной типизации от статической и динамической типизации многое ставит на места.

Да, вполне номинативны.


class A:
  pass

class B:
  pass

print(print(type(A) == type(B))) # False

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


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

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

(Из-за какого-то бага комментарий выше оказался не дописан)


Да, вполне номинативны.


class A:
  pass

class B:
  pass

print(print(type(A) == type(B))) # False

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


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

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


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

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


Например, пользователь написал x.foo(). Значит ли это, что в библиотечном интерфейсе появился метод foo? Нет. Этот метод там может быть (возможно в виде недокументированного кода или бэкдора), и уже является частью интерфейся, либо его там может не быть вообще. В обоих случаях интерфейс сформирован автором библиотеки.


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

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


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

Допустим, в библиотеке есть некая функция Library.foo, которая принимает объект Library.IMyInterface.
Автор Library.IMyInterface мог потребовать реализации кучи разных методов, которые на самом деле в функции foo никогда не вызываются. Явное определение интерфейса практически всегда накладывает более строгие условия на получаемый функцией объект чем это необходимо. При динамической/структурной типизации требуется реализовать только то, что реально используется. Формально можно сказать, что для каждого метода, библиотеки, предназначенных для работы с однотипными объектами, требуемые интерфейсы этих объектов будут различны и всегда минимальны в противоположность явному описанию интерфейса.
При динамической/структурной типизации требуется реализовать только то, что реально используется.

Уточнение: при динамической и структурной типизации.

Допустим, в библиотеке есть некая функция Library.foo, которая принимает объект Library.IMyInterface.
Автор Library.IMyInterface мог потребовать реализации кучи разных методов, которые на самом деле в функции foo никогда не вызываются. Явное определение интерфейса практически всегда накладывает более строгие условия на получаемый функцией объект чем это необходимо.

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


В итоге самый простой способ просто передавать PDFDocument объект в каждый метод, потому что он умеет всё.




Собственно, я бы не отказался от примера любой популярной библиотеки, которая требует больше чем ей нужно. Обычно (особенно при активном использовании генериков) функция выставляет минимально рабочий интерфейс foo<IHasFoo + IHasBar + IHasBaz>() и дальше вы с этим делаете что хотите.

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

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

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

IHasFoo + IHasBar + IHasBaz — это очень хорошо. Это явное минимальное описание интерфейса. То есть автор подумал об интеграции и даже задокументировал своё решение в код. Проблема PDFDocument в том что автор не подумал об интеграции. И в номинальном и в структурном исполнении можно найти хорошие и плохие примеры. Я же, впрочем, утверждаю, что сделать хорошую интеграцию на явных интерфейсах сложнее чем на неявных. Это требует меньше телодвижений. Но и в том и в другом случае это требует проектирования, конечно.
В качестве примера можно привести многие библиотеки обработки данных и целые фреймворки, завязанные на свои внутренние типы. Возьмём хотя бы Qt. Методы Qt принимают QString, QList, QHashTable, хотя по хорошему, они должны принимать не эти типы, а все похожие на… В результате, при интеграции Qt с прочими библиотеками в точках сопряжения появляется довольно много лишних операций преобразования типов.

Ну это как раз-таки пример не очень хоршего проектирования. В типичной джаве там были бы IQString, IQList и так далее. В более интересных языках был бы тайпкласс QString/QList/..., которые можно было бы реализовывать для чужих библиотек.


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

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


data Foo = Foo { baz :: Int, bar :: Int } deriving (Show, Generic)
data Bar = Bar { baz :: Int, bar :: Int } deriving (Show, Generic)

instance Convertible Foo Bar
instance Convertible Bar Foo

main :: IO ()
main = do
  let foo = Foo 10 20
  print foo
  print $ (convert foo :: Bar)

Если интересно — репл.


Соответственно чтобы использовать любые структурно похожие типы клиенту достаточно написать instance Convertible SomeThirdPartyType MyType.


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

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

Насколько это критично в инженерной практике — вопрос ситуативный.

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

P.S. Я позволю себе процитировать одного хорошего человека: «В инженерном деле нет ничего хуже, чем религия». Так будем же как и всегда помнить об особенностях инструментов и сообразно их применять.
Успех питона, баша, перла, а также текстовых форматов хранения данных (см. Искусство программирования для Unix) в качестве компонентов системной интеграции, показывает, что как минимум в некоторых задачах динамика превосходит статику.

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


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


P.S. Я позволю себе процитировать одного хорошего человека: «В инженерном деле нет ничего хуже, чем религия». Так будем же как и всегда помнить об особенностях инструментов и сообразно их применять.

Согласен, писать CI скрипты на хаскеллях я бы не стал. Однако стоит задуматься о проекте хотя бы на 500+ строк, и тут уже возникает желание описать контракты, чтобы потом не сидеть с дебаггером и не писать тесты на очевидные вещи.

UFO just landed and posted this here
(я аж ссылку специально сохранил, чтобы зайти сюда днём с десктопа и прокомментировать)

«Успех питона, баша, перла, а также текстовых форматов хранения данных (см. Искусство программирования для Unix) в качестве компонентов системной интеграции»

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

Не то чтобы я был в восторге от докера, я бы вообще с удовольствием посмотрел на то, как группы фронтендеров массово будут паковать свои js-поделия в .deb, но ведь они же не хотят этим заниматься, а докер притаскивают на каждый чих.
UFO just landed and posted this here
Для каждого случая есть свой лучший способ реализации.
«Программисты делятся на 10 два типа — духовные последователи Платона и духовные последователи Гераклита.
Платонисты верят в идеальные формы, любят, когда компьютер делает именно то, что ему говорят, и готовы пойти ради этого на любые средства. Всякая неопределённость в поведении должна быть устранена, все побочные эффекты учтены, все входы и выходы записаны — иначе для чего нужны компьютеры, как не для того, чтобы железной логикой быть источником порядка среди непредсказуемых людей? Платонисты придумали статическую типизацию, конечные автоматы, таблицы переходов, Агду, формальную верификацию и соответствие Карри-Ховарда. Когда они подходят к задаче, их мечта — найти именно такую структуру, в которую эта задача идеально влезает. Идеально! Платонистов чаще всего можно встретить в embedded, разработке компиляторов, проектировании сверхнадёжных систем (авионики, например), hard real-time, микроядрах — в общем, чем хардкорнее, тем лучше.

Не таковы гераклитяне. Они-то знают, что совершенства не существует, что миром правит хаос, и нет никакого способа привнести порядок туда, где его не было и не может быть никогда. Неожиданности всегда появляются, системы всегда ломаются, учесть всё невозможно, и единственный способ выживать в таком мире — быть достаточно гибким и изворотливым, чтобы восстанавливать всё утраченное. Гераклитяне придумали позднее связывание, аннотации, юнит-тесты, прототипы, горячую замену кода, нулевые указатели, message passing, акторы и супервайзеры. Ну и Perl, само собой. Их мечта — чтобы всё хоть как-то работало, какой бы хаос ни происходил вокруг и какими бы безумными ни были начальные условия и входные данные — а следовательно, гераклитян часто можно встретить в big data, финансах, вебе, телекоммуникациях, devops и других местах, где правит бал Его Величество Случай.»
А Пифагорейцы изобрели функциональную парадигму :).
Сложно сказать, но принципы ООП применялись еще во время древних царств задолго до того же Пифагора. Думаю, пальму первенства формализации ООП можно отдать Демокриту с его идеями как прообразами вещей. Ну, а дальше эта концепция проходит через руки того же Платона и попадает к христианским философам. В целом ООП — очень древняя концепция, происходящая из особенностей работы мозга, а именно необходимости к обобщению, поэтому, я бы сказал, что ООП было всегда.
Самая веселуха — я три года писал на плюсах (билдер), прекрасно понимаю принципы и когда на новой работе в конце стажерки сказали прочитать лекцию по ООП — я реально вспотел пока въехал в статью на интуите, моя лекция получилась глубоко теоретическая и мозгодробительная, а в последующей лекции, которую читал уже руководитель я осилил только теорию, практику так в общем ни не начал.
После этого основы ОО проектирования я даже боюсь открывать.
Хм, а что вы имеете ввиду под «основы ООП»? Все основы на википедии влезают на полстранички: wiki

Читать лекции по статьям в вики так себе затея ))
www.intuit.ru/studies/courses/71/71/info Тут чуть больше) 17 лекций, по крайней мере я сам понял зачем применять ООП.
Ну зачем нужно ООП на мой взгляд великолепно обьяснено в соседнем комментарии. А лекция… Я бы взял основы с вики и показал для них парочку небольших примеров.

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


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

То есть кто такой Алан Кей вы не знаете, а как лекции по ООП читать — пожалуйста.

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

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

Кей изобрёл то что изобрёл(youtube, интервью), и назвал это ООП, а было до появления Java, сильно до. Actor Model продолжение тех идей «ООП».

Путаница пошла когда все подряд, в т.ч. Java, C++ и пр. начали использовать модное слово чтобы привлекать больше аудитории.

То что самому Кею название ООП не нравится — да. Вот его «извинение» от 97г. — youtu.be/oKg1hTOQXoY?t=2270, но историю не переписать.

А в данном случае проблема то не в определении, проблема в том что из-за путаницы термином ООП называют всё подряд, и лекции «по ООП» вне исторического контекста смысла не имеют, а когда сами лекторы этого не понимают начинается всякая чушь вроде примеров ООП в виде наследования Moderator от User и пр.

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

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

И какое такое общепринятое определение?
3 Кита — вздор, и с ними полно людей несогласно, да и в чём практическая ценность — не ясно.

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

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

А что с практической ценностью?
Чего должно дать это определение кому-то? Зачем оно студентам?

Почему не дать просто принципы проектирования?

А определения принципам не противоречат.

А определения принципам не противоречат.

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

Часто преподают программирование те, кто собственно профессионально работает программистом, или кем-то кто программирование широко применяет в работе.
Вот только не у всех профессионально работающих программистов есть хоть какие-то педагогические навыки. Ну и кто широко применяет программирование — архитектор БД — он вообще будет тратить своё время на преподавание?
Вот только не у всех профессионально работающих программистов есть хоть какие-то педагогические навыки.

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

Ну и кто широко применяет программирование — архитектор БД — он вообще будет тратить своё время на преподавание?

Все, или большинство — не будут. Но как показывает практика, такие люди находятся. Я сам понемногу преподаю (не программирование правда) просто потому, что нравится процесс; знаю несколько однокурсников, кто так же делает.
В дополнение к интересу у некоторых может быть и план научить + заинтересовать студентов, чтобы они шли в вашу команду/фирму.
Студент очень часто заинтересовал лишь халявой, ибо с первого курса работает и хает систему высшего образования что там не дают «современных знаний». А по поводу педстажа — поверь, умение «на пальцах» доносить сложные вещи очень важно. У нас к примеру завкаф лекции читал — так там формула на формуле, до меня лет через 5 неравенство Белла только доехало. Зато в науке у него все норм было.
Слишком сильное обобшение про студентов. Понятно, что люди разные бывают — но адекватных студентов хватает для того, чтобы было собственно интересно преподавать.
Конечно, собственно для них и преподаешь. Это в школе всех надо учить, со студентами проще. Но от зачета на халяву ни один российский студент не откажется.
Прям такого эксперимента мы не проводим :) Но регулярно заметное количество студентов переводится от преподов, про которые известно что они халявные, к другим (в обратную сторону, конечно, тоже часть переводятся). Ещё по некоторым предметам есть два уровня — условно «базовый» и «продвинутый», и в базовом меньше тем/задач; часть студентов выбирает продвинутый, безо всякого принуждения. Нигде в оценке не пишется, какой из уровней сдан.
Кстати, именно книга Бертрана Мейера меня вывела из болота ООП на свет ФП.
UFO just landed and posted this here
Иногда за обилием теоретических выкладок не видно тех идей, которые лежат в основе того или иного метода.

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

Всё ООП состоит в одной единственной идее, или точнее в одной единственной проблеме.

Нам сложно осознать программу целиком. Программа необозрима и разнородна. И тогда мы принимаем естественное решение. Мы начинаем программу дробить на части и работать с частями независимо.

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

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

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

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

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

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

Как я уже написал выше дело скорее не в изменениях, а в том насколько громоздки и сложны имеющиеся структуры.
Структуры вполне в С используются. И опять же — когда мы замораживаем требования на весь процесс разработки — можно обойтись и без ООП. Но да, с определенного размера ПО заморозить требования = ПО будет устаревшее на момент выхода.
Структуры вполне в С используются.

Ну под «структурами» я в данном случае имел ввиду нe «struct» из ЯП, а те «части реального мира», которые нужно моделировать.

И опять же — когда мы замораживаем требования на весь процесс разработки — можно обойтись и без ООП.

Даже если забыть о том что «замороженные навечно требования» я ещё никогда в своей профессиональной карьере не встречал, то всё-таки надо понимать что проблема не только в этом. Если вам дадут ТЗ, в котором описание структур и их взаимодействия между собой будет по обьёму больше чем «Война и мир», то функциональные ЯП в этом случае будут не особо «оптимальны».
Но может кто-то видит это и по другому…
взаимодействия между собой будет по обьёму больше чем «Война и мир»

Вы когда вызываете функцию, скажем, foo, вы ожидаете какого-то результата? Или вы просто вызываете случайные функции у случайных объектов и надеетесь, что результат пройдет QA?

Я не уверен что ваш комментарий пришёл по адресу. Я то как раз ничего против ООП и строгой типизации не имею :)

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


От чего оно вдруг должно развалиться?

У вас в ФП точно так же есть объекты, методы, есть инкапсуляция поведения за функциями и тайпклассами.

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

Я вроде статью писал, идея ФП в ссылочной прозрачности. Поэтому да, не обязательно оно там будет, но как-то получается что на практике все фп языки которыми кто-то пользуются (scala/haskell/...) их имеет.

Я знаю людей, которые вовсю пользуются Scheme. И там с ООП по моей памяти не то чтобы всё особо розово… :)
Ха))) Я встречал и не один раз. И не два. Всякие там лабы и прочие курсовые, небольшие утилиты. Опять же матмоделирование и прочая «наука». В общем тоже имеет место быть. А когда ООП ради ООП начинают лепить, а особенно гугль-программисты… У нас тут прилично легаси-кода, на котором коллеги учились ООП программировать. Собственно примеры как не надо перед глазами))
А когда ООП ради ООП начинают лепить, а особенно гугль-программисты…

Большинство людей просто при прочих равных берут тот ЯП, который им более знаком и удобен. И по хорошему 90% всех задач одинаково хорошо/плохо решаются различными ЯП. то естъ это просто скорее дело вкуса.
Хз. Мой диплом быстрее работал на фортране, специально проверял (чем на с++). Но параллелил код я конечно на сях. Опять же для бизнес задач — кобола (abap) вполне достаточно, но не думаю что кто нибудь начнет в здравом уме писать на нем драйверы. А на плюсах реализовывать свою веб страничку — кмк к пенсии только превед медвед на 80 порту наваяешь))
Ну совсем до абсурда дело доводить конечно тоже не надо. Под «разными ЯП» я всё-таки имел ввиду не абсолютно всё множество имеющихся ЯП, а те из них которые приведут к удобоваримому результату за приемлимый отрезок времени :)
Давай тогда остановимся что для каждого класса задач есть свои наиболее оптимальные языки. Грубо говоря — питон пхп джиэс для веба, с/asm для драйверов, с++ для разработки ОС и всякие там лиспы для имакса. И уважающий себя программист должен знать несколько языков желательно для нескольких сфер. Тогда выбор будет более оптимальным.
UFO just landed and posted this here
UFO just landed and posted this here

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


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


Парадигмы программирования можно рассмотреть как некое подмножество приёмов программирования в комбинации продуцирующие код с некоторыми свойствами.


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

Возвращаясь к первому коменту в ветке — входные данные у нас не всегда предсказуемые, поэтому в команде разработчиков нужны и платонисты и духовные последователи гераклитяне. имеет место быть и статическая и динамическая типизация))
Я не совсем понимаю что значит «непредсказуемые данные». У вас есть какие-то данные, которые сами содержат динамический код, который их должен обрабатывать?
Или у вас всё-таки обработка проходит в «вашем» коде?
" Их мечта — чтобы всё хоть как-то работало, какой бы хаос ни происходил вокруг и какими бы безумными ни были начальные условия и входные данные — а следовательно, гераклитян часто можно встретить в big data, финансах, вебе, телекоммуникациях, devops и других местах, где правит бал Его Величество Случай."
А это то здесь причём? Если у вас где-то написан код, который каким-то особым образом обрабaтывает какие-то «непредсказуемые данные», то эти данные на мой взгляд уже нельзя считать непредсказуемыми. И следовательно можно типизировать.
Я тебе как физик скажу, что вселенной правит хаос. То есть результат работы кода уже непредсказуем (точнее предсказуем, но с определенной вероятностью). Или например квантовые компьютеры — там с кубитами и забитами вообще веселуха.
Я тебе как физик скажу, что вселенной правит хаос.

Это не значит что хаос должен править в ЯП.

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

Это уже пошла философия, которая очень далеко выходит за рамки обсуждаемой темы :)
Почему нет? Как программистов можно условно разделить на платонистов и гераклитян, так и яп можно разделить на и «статические» и «динамически» ориентированные)) Я кстати больше приверженец платона, но уже давно не перфекционист.
Делить вы ЯП можете как вам угодно. Ваше личное дело. Но эти ваши «непредсказуемые данные» будут одинаково «непредсказуемы» и там и там. И об этом вобщем-то и речь в статье…

К слову, цитируя Пирса


Terms like "dynamically typed" are arguably misnormers and should probably be replaced by "dynamically checked", but the usage is standard.

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

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

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

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

Таким образом, ФП может работать вместе с ООП при условии иммутабельности объектов.

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

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


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

Мне немного непонятно, почему класс и метод меняют свои смыслы, но да шут с ними...


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

Я согласен, что ООП и ФП не противоречат друг другу.


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


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

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

Ну хорошо, я рад, что мы понимаем друг друга :-)

А кстати, кто-нибудь может кратко пояснить, где в иммутабельном ФП прячут хранение состояния?

UFO just landed and posted this here

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


Пример:


import qualified Data.Map as Map

main = do
    let hm = buildMap
    putStrLn $ "hm = " <> show hm

buildMap =
    let x1 = Map.empty
        x2 = Map.insert "a" 1 x1
        x3 = Map.insert "b" 2 x2
    -- x1 and x2 will be garbage collected
    in x3

Вот, можете посмотреть эту презентацию, начиная с functional list-а, там, я надеюсь, всё достаточно очевидно.

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

Только ООП в том определении в котором этот термин появился ничего общего с этими идеями не имеет.
См. —
wiki.c2.com/?AlanKaysDefinitionOfObjectOriented и www.youtube.com/watch?v=fhOHn9TClXY

P.S. Все лекции по ООП которые называют наследование одним из главных принципов — только вредят.
Современное ООП сильно отличается от идей Алана Кея… Да, отличается.
Вообще, изначальная формулировка, данная господином Кеем имеет скорее историческое значение.

И да, наследование оказалось не очень удачной концепцией. Но методология парадигмы ООП не статична. Она развивается и изменяется. Это нормально.
Современное ООП сильно отличается от идей Алана Кея… Да, отличается.

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

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

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

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

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


Ну… Это довольно сложная и нестандартная тема…

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

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

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

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

Но это всё… Немного за границами настоящего обсуждения.

ОМГ, магия! Чур меня!

Строго говоря, есть всего одна парадигма программирования, которая имеет что-то вроде определения — структурное программирование.

Разве? Я вполне удовлетворен определением, что программа написана в ФП стиле если она состоит из чистых функций, тогда как


An expression e is referentially transparent if, for all programs p, all occurrences of e in p can be replaced by the result of evaluating e without affecting the meaning of p. A function f is pure if the expression f(x) is referentially transparent for all referentially transparent x.
Я безусловно согласен с тем, что среди всех остальных парадигм ФП наиболее похожа на законченную концепцию.

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

Думаю лет через десять можно будет уже сказать точно.

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


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

Ну чтож. С цель развеивания мифов и популяризации ФП, так и запишем. СУЩЕСТВУЮТ ДВЕ ПАРАДИГМЫ ПРОГРАММИРОВАНИЯ, КОТОРЫЕ ИМЕЮТ ЧТО-ТО ВРОДЕ ОПРЕДЕЛЕНИЯ. Пусть все услышат и пусть отныне будет так.

:-)
Я бы сказал, что ООП декомпозирует статику (картину мира), а ФП действия (алгоритмы работы с потоками данных). И кмк вся проблема в том, что «религиозность» мешает создать правильный их сплав.
upd

Мне больше нравится вывод этого доклада:


When I initially wrote this blog post, it really bothered me that I couldn’t come up with a good name for layers 2 and 3. I published it anyway because it’s useful enough with just the numbers, and names have the potential to mislead. I’ve since realized that the layers already have names!
.
  • Imperative programming
  • Object Oriented programming
  • Functional programming

Если доклад долго то вот заметка докладчика.

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

Во-вторых: в ООП работа с данными, имеющими действия происходит только когда у методов вообще нет параметров. Если у методов есть параметры, получается работа с данными, имеющими действия, которые в свою очередь имеют данные. Если говорить в ваших терминах.
Ну… Я пожалуй просто сошлюсь на книжку «Исскуство програмирования для Unix», на которую я уже ссылался в этом треде. Там где-то было достаточно подробное рассмотрение того, почему данные требуют внимания больше, чем алгоритмы.
Отсюда прямо следует идея инкапсуляции объектов, идея интерфейсов, идея переиспользования кода в том числе в виде наследования и все прочие идеи ООП.

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

Идей у наследования две.

Первая — вынести и переиспользовать часть кода классов кодирующих схожие объекты.

Вторая — идея интерфейса — использовать родительский тип как интерфейс к многим разным вариантам имплементации.

Впоследствии идеи разнесли в концепции миксинов и интерфейсов… Или кто там их как называет, поскольку совместно они работали не очень хорошо.

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

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


Вторая — идея интерфейса — использовать родительский тип как интерфейс к многим разным вариантам имплементации.

Это вполне решается классами типов в Haskell (1990). Или, если копнуть глубже, модулями в Standard ML (1983).


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

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

UFO just landed and posted this here

А я вам больше скажу… Довольно сложно писать функционально и при этом необъектно.

А здесь не путаются понятия «акторы» (более широкое определение) и «объекты» (узкое)?

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


Я акторы воспринимаю немного по-другому. Скажем так, в функциональном программировании понятия времени нет, а в акторной модели понятие времени есть.


Но это эфемерно как-то...

UFO just landed and posted this here
Мне всё нравится, мне optionals не нравятся. И двойственность — то, что категорически запрещено в рамках стандартной библиотеки, легко дозволяется в UIKit, поскольку он на деле ObjC в свифтовой обертке.
Для меня, и думаю для большого числа других разработчиков, ценность динамической типизации заключается всего лишь в том, что нужно писать меньше кода. Да, на статических типах можно написать систему любого уровня сложности, но придётся везде пробрасывать типы, создавать не всегда нужные зависимости на них и т.п… К сожалению реальность такова, что я сам, как сторонник динамической типизации, в реальных проектах часто применяю TypeScript, так как:
— реально слабопрогнозируемые системы приходится разрабатывать редко
— типы спасают от ошибок по невнимательности, пусть и ценой лишнего кода
— и главная причина: от ошибок по невнимательности не спасает IDE. К сожалению, в 2020 году, когда машины распознают дорожную разметку и пешеходов, IDE всё ещё не способны распознать тип переменной в программе. Я убеждён, что нет причины для программиста писать Int или String на каждое действие, всё это можно делегировать машине. Динамическая типизация это прекрасная высокоуровневая концепция, но инструменты для JavaScript в наше время находятся на уровне ниже, чем инструменты .NET в 2010м. В итоге получается прекрасная технология, к которой приходится добавлять сурогаты из за того, что машинам слишком сложно в ней работать.

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

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

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

Таким образом достигается комбо преимуществ статической и динамической типизаций. Аннотациями можно полностью покрывать швы. А в локальных участках, где срок жизни какой-то переменной составляет 5 строчек, типизация почти никогда не требуется, особенно, если использовать правильные названия идентификаторов типа attempts_count (попробуете угадать тип?). И даже если по имени тип непонятен, просто поднимите взгляд на пару сантиметров вверх (это на крайний случай). А уж если вы хотите попыткам добавить разных таймаутов, то попробуйте вот это переписать на сишарп (вы же на нем пишете?):
for timeout in [1, 3, 10]:
    ...

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

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

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

И прежде, чем со мной спорить, попробуйте меня понять, вы же видите, что я не фанатик, и не делю мир на черное и белое.
UFO just landed and posted this here
У вас, кажется, ложная дихотомия. В локальных участках и на статически типизированных языках аннотации типов совсем не обязательны, компилятор их часто может вывести.

Получается, вроде все согласны, что:
— в локальных участках типизация часто не нужна и можно де-факто использовать динамическую даже в статически типизованных языках
— при взаимодействии большого числа сложных модулей типизация полезна, хотя бы даже как документация, в виде типов, аннотаций или розолвинга типов от IDE (наступит же когда-нибудь это светлое будущее)
Получается спор об одном и том же, разница только в том, какой подход использовать по дефолту.
UFO just landed and posted this here
Просветите какую вы видите разницу? Я вижу только ту, что в некоторых языках нельзя менять тип переменной, даже объявленной неявно, что не выглядит существенным в данном обсуждении.
UFO just landed and posted this here
Во фронтенде одна из самых распространённых операций — это приведение чего-нибудь к строке, чтобы вывести пользователю, поэтому в JavaScript и сделано приведение типов автоматическим.
Хотя в том же C++, насколько я помню, ещё интересней и перегрузка операторов позволяет определить применение любых операторов к любым парам типов, хотя типизация там вполне статическая. Так что не сказал бы, что эта особенность языков жёстко связана с динамической или статической типизацией.

Перегрузка операторов есть и в расте, поэтому там можно складывать строки с числами, и он скастует автоматически (если написать соответствующий ньютайп).


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




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

Кстати именно динамическое преобразование «чего-то» в строку работает много где. То есть если вы конкатенируете какой-то объект со строкой, то на объекте «имплицитно» вызывается какой-нибудь ToString().

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


Я буквально на прошлой неделе правил в сишарпе место которое в лог писало [object Object] потому что человек тустринг переопределить забыл, и вместо сообщения об ошибке писалось очень полезное My.Company.Name.UsefulException'2.

Я буквально на прошлой неделе правил в сишарпе место которое в лог писало [object Object] потому что человек тустринг переопределить забыл, и вместо сообщения об ошибке писалось очень полезное My.Company.Name.UsefulException'2.

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

С другой стороны то что в JS творится это на мой взгляд уже явный перебор.
UFO just landed and posted this here
Статическая типизация — это значит что тип известен и он не меняется. Его необаязательно задавать, посмотрите например на var в java-там нету динмаической типизации, но есть var
var или int использовать — это спор о явной и неявной типизации.
Автоматическое приведение типов этого также не отменяет(опять же java -хороший пример)
В целом же разговор в статье идет именно про статическую vs динамическую систему типов.
— в локальных участках типизация часто не нужна и можно де-факто использовать динамическую даже в статически типизованных языках
Ну на СиШарпе вы можете написать:
var i = 123;
var s = "hello, world"

Чем это хуже JS?
Вы путаете неявную аннотацию и динамическую типизацию. Здесь тип выводится на этапе компиляции, и после того, как он был выведен, работает строгая типизация. На c# вы не можете написать:
var i = 123;
i = "whatever";
Я ничего не путаю.

Зачем такое писать я не знаю? Даже в JS такое говно не пропустят линтеры.
Я спрашиваю, про код на практике.
Всё вы путаете, и я не понимаю чего вы добиваетесь и что пытаетесь доказать.
Повторю ещё раз: var не попадает в сборку, он существует только в тексте и, по итогу, в сборку попадает тип, который додумал компилятор. Он статичен, и не меняется во время выполнения.
Ну? И что? Как это влияет на программиста? От того, что там внутри компилятор что-то додумал — вам внезапно хуже программировать стало?

Я отвечал на конкретный тезис:
— в локальных участках типизация часто не нужна и можно де-факто использовать динамическую даже в статически типизованных языках

Я считаю, что неявная аннотация для таких случаев прекрасно подходит.
Действительно, какая разница, сижу я на кровати или пью ли чай. И то и то по кайфу.
Я пытаюсь сказать, что строгая типизация за неявной аннотацией всё ещё строгая. Вы пытаетесь поставить ненужную параллель с динамической только из-за того, что вам не нужно писать такое:
std::vector<std::array<std::future<std::function<void(std::shared_ptr<CSomeClass>)>>>, 2>>

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

Вполне можете:


object i = 123;
i = "whatever";

Правда у меня один вопрос — зачем?

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

компилятор их часто может вывести

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

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

Если есть статический язык, в котором статичность везде опциональна, а не только в локальном контексте, я только за, потому что хотелось бы самому решать.
UFO just landed and posted this here
Я ради интереса попробовал сделать что-то с mypy.
Мне рабинович рассказывал, что программа, которую он написал на Java, постоянно тормозила и глючила. С тех пор я знаю, что статическая типизация плохая. Извините. Я просто не понял к чему вы это написали. Давайте точнее, прям так и скажите: «я увтерждаю, что все линтеры плохие и не способны работать со сложными типами». А потом еще добавьте: «Поэтому динамическая типизация плохая»

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

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

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

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

Мне по наследству достался прекрасный проект, обильно обмазанный динамичностью и полным отсутствием типов. Вплоть до того, что объекты одного семейства имели разные наборы методов. Сейчас я все это рефакторю, знаете какой это ад? Я чувствую себя настоящим героем. Но почему-то вы не можете поверить, что по мере рефакторинга код становится читаемым, в среде появляются подсказочки, а в рантайме перестают вываливаться ошибки, что метод отсутствует или переменная не определена.
UFO just landed and posted this here
Вы ведь понимаете, что линтеры — это такой маленький кусочек статической типизации
Понимаю, я об этом и говорю. Линтеры подарили мне статическую типизацию, сохранив динамику в рантайме. Слава линтерам!

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

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

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

Кстати, про производительность согласен. Но это отдельный разговор.

То есть, понятно, что можно взять питон, начать с ним героически воевать [...] Но зачем, если можно сразу взять нормальный статически типизированный язык?
На это я тут уже отвечал несколько раз. Опять таки, если вы не верите в предыдущий мой ответ, на это тоже я не могу отвечать.

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

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

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

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

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

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

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

Если да, то и это уже давно не так:
1. Линтеры уже встроены в некоторые IDE для динамических языков, и работают из коробки.

2. Статический язык не подразумевает автоматическое наличие линтера. Вы так же должны поставить IDE, в которой уже есть линтер. Кроме того, линтеры для статических языков существуют и как отдельные продукты, которые тоже сами себя не поставят.
Линтеры уже встроены в некоторые IDE для динамических языков, и работают из коробки.

То есть вы вообще ничего не делаете и у вас магическим образом появляются проверки на соответствие структур? Для абсолютно любых структур и типов? Я хочу такой линтер из коробки, не подскажете где взять? :)
Статический язык не подразумевает автоматическое наличие линтера.

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

А если в каком-то модуле вам требуется творить динамическую магию, пилить всякие супер прозрачные прокси с произвольными неизвестными аргументами, метаклассы, и прочую динамическую хрень, то такой модуль можно внутри не полностью типизировать, а к проекту присоединить через адаптер со статическим интерфейсом.
Ну нет, типы то я должен расставить, где требуется. Иначе описанная мной схема не работает.
То есть вы рассказываете, что подключив линтер и вручную расставив аннотации типов вы получаете ровно то же самое, что в статически типизированных языках работает само (без линтера и аннотаций).
Да, наверное так и есть. Спасибо за наглядное объяснение преимуществ статически типизированных языков. Стоит еще отметить, что забытая анотация типа не позволит сломать проверку типов в статическом языке.
То есть вы рассказываете, что подключив линтер
Я еще раз повторяю, раз вы не потрудились прочитать, я НЕ подключаю линтер, я просто ставлю среду, как и вы ставите среду для статического ЯП.

и вручную расставив аннотации типов
А в статическом языке типы расставляются как-то не вручную? Там педальный привод в комплекте с IDE поставляется?

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

Ну если посмотреть на вещи вроде генерации типов из каких-нибудь xsd…

А в статическом языке типы расставляются как-то не вручную?

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


Там педальный привод в комплекте с IDE поставляется?

В случае с Java — в общем, да.

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

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


Так и в нормальных линтерах, которые уже встроены в IDE, есть вывод типов. И поэтому это не отвечает на вопрос, какую же работу за меня выполняет статический ЯП в этой ситуации. (когда есть аннотации + хороший линтер)
Ну как бы ради этого оно и создавалось, чтоб применять с умом. А если без ума, или память плохая, то да, лучше конечно с ним не работать.
Ну то есть у статических языков более широкая область применения. Их можно применять в тех случаях, когда динамические языки применять не стоит (например, если память плохая). Об этом и речь.
Их можно применять в тех случаях, когда динамические языки применять не стоит (например, если память плохая). Об этом и речь.
Ещё не стоит применять динамику, если у вас с членами команды не гештальт-сознание и вы не можете с лёгкостью получить все их воспоминания. И воспоминания всех разработчиков всех библиотек, которые используете.
Ещё не стоит применять динамику, если у вас с членами команды не гештальт-сознание
В этом случае статику тоже не следует применять. Лучше вообще не заниматься разработкой, потому что типы — это лишь одно из мест, где можно накосячить.

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

Но у вас всё ещё остаётся возня с самим линтером. И про «замечателъные варианты из коробки» мне рассказывать не надо, я сам с линтерами работаю и там далеко не всё так замечательно как вы пытаетесь представить.

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

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

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

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

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

Но у вас всё ещё остаётся возня с самим линтером. И про «замечателъные варианты из коробки» мне рассказывать не надо, я сам с линтерами работаю и там далеко не всё так замечательно как вы пытаетесь представить.
То есть опять все сводится к одному аргументу — для динамических языков нет качественных линтеров, с которыми не надо было бы возиться, я правильно понял?
Добро пожаловать в мир динамической типизации. Раз в статические языки закладывают такую возможность, значит давайте не будем утверждать, что это никому не нужно?

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

Я уже говорил «я не встречал» — это не аргумент, либо аргумент не для такого уровня беседы.

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

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

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

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

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


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

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

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

Могу ли я попросить перечислить критерии качественного линтера?

Ну кроме перечисленного вами хотелось бы ещё как минимум нормальную интеграцию в любые IDE и CI. И гарантированная обратная совместимость. И хоть какая-то гарантия что разработчики линтера в очередной раз на него не забьют и не придётся мигрировать на другой.

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

Конечно можно. Но в них отсуствует целая категория ошибок, которые есть в языках с динамической типизацией. И это заметно упрощает работу для «иерархии сеньоров, тимлидов и архитекторов». И в общем-то об этом и идёт речь: примеры преимущества статической типизации вам приводят здесь уже неоднократно, а вот примуществ динамичeской вы пока особо привести и не можете.
Если вы его приведётё, то у меня в копилке появится самый первый пример.
Разве ваша личная субъективная копилка — это отражение реального мира ИТ?

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

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

нормальную интеграцию в любые IDE и CI. И гарантированная обратная совместимость.
Серьезные требования. Полагаю, все статические языки им удовлетворяют?

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


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

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

а вот примуществ динамичeской вы пока особо привести и не можете
Я приводил пример. На что меня попросили привести пример, в каких задачах это нужно. А потом бы меня попросили привести пример, в каких проектах нужны такие задачи и т. д. Собственно, о чем я и говорил, приводя глупый диалог про труп.
Разве ваша личная субъективная копилка — это отражение реального мира ИТ

Для меня да. По крайней мере другого у меня нет.

И поэтому мой пример ничего не изменит.

Ну так вы его дайте и мы посмотрим.

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

Как минимум некоторые да. А вы можете привести пример хотя бы одного единственного линтера который это тоже делает? :)

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

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

Я что-то пропустил?
Ну так вы его дайте и мы посмотрим
Не даду! Я не могу углубляться в такую бесконечную беседу, не имею столько времени. Все свободное время уходит на отладку ошибок типизации и борьбу с линтером!))

А вы можете привести пример хотя бы одного единственного линтера который это тоже делает? :)
О нет, я спросил про критерии линтера, чтобы убедиться, что мои требования ниже, и что для вас действительно хороших линтеров нет. Среди моих требований только, чтоб с ним не нужно было отдельно возиться, и чтоб оно мне подсвечивало ошибки типов в среде (хотя бы 95%), и чтоб автодополнение работало. Ваши требования я понял, они весьма справедливы.

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

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

Я что-то пропустил?
Ну я приводил примеры первого уровня, от меня попросили примеров второго уровня (что было предсказуемо).

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


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

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

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

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

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

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

Вообще, сейчас очень активно заняла позицию тенденция движения в сторону гибридных языков. Большинство современных языков стараются обуниверсаливаться: в ООП языки завозят ФП функционал, в динамические языки завозят аннотации и линтеры, в статические — рефлексию и вывод типов. Эту тенденцию видно невооруженным взглядом, потому что на нее есть запрос. И это прекрасно, многие мечтают абстрагироваться от того, что под капотом. Это снизило бы общий порог вхождения, одновременно расширив сферу собственной компетенции как специалиста. Хоть пока это и звучит как утопия, но даже сейчас мы видим какой-то прогресс и тенденцию.
Если нужен, например, какой-то офигенно прозрачный и гибкий прокси,
Не лучше для таких крайне редких случаев в виде исключения использовать рефлексию со статикой?
Не лучше для таких крайне редких случаев в виде исключения использовать рефлексию со статикой?
Я не считаю, что это лучше или хуже в той мере, что нужно жертвовать другими факторами при выборе инструмента. Если бы мне было пофиг на дизайн языка, гибкость, синтаксис, экосистему, и плюс кучу всяких мелочей, я бы просто взял статический.

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

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

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

А чем они отличаются от типов?


Таким образом достигается комбо преимуществ статической и динамической типизаций.

Скорее минусы обеих. Посмотрите на драму с актиксом, она это наглядно показывает. Как только какие-то жалкие 5-10% кода начинают забивать на типы, то вся программа разваливается как карточный домик. Поэтому нет, any через any с опциональным декорированием типов — это не "лучшее из двух миров".


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

Пожалуйста


foreach (var timeout in new[] {1,3,10))
   ...

Хоть сишарп далеко не эталон статически типизированных япов, но даже на нем не вижу особой проблемы, по крайней мере в этом примере. Тут подробно рассказано почему много писать типов не надо. Это трудно замерить, но по моим прикидкам типы едва ли 10-20% кода, потому что они участвуют только в топ декларациях: сигнатурах публичных функций (для приватных можно забить) и публичных же структур (вместо приватных можно перекидываться кортежами, например).


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

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


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

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

А чем они отличаются от типов?
Так я о том и толкую! Ну то есть различия конечно есть, но в контексте этого вопроса, ничем не отличаются.

Скорее минусы обеих. Посмотрите на драму с актиксом

Что есть актикс? Ничего не понял. Не могу ответить.

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

Благодаря этому, роль статической типизации размывается, и мы можем позволить себе выбирать язык по другим факторам, расширяя круг поиска.
UFO just landed and posted this here
Нет, это так не работает. Нельзя что-то доказать наполовину.
А я ничего и не доказывал, но раз вы хотите: из пункта А выезжает неизвестная переменная (это может быть чужая либа без аннотаций, или HTTP-API, что угодно), где она проходит валидацию в адаптере на этапе рантайма. Адаптер на входе имеет неизвестный тип, а на выходе уже известный, что и отмечено в аннотации. Дальше между нашими слоями мы доверяем этой аннотации на основе доверия адаптеру.

Только не надо говорить, что проще взять сразу статический язык, я на это уже тут несколько раз отвечал.
UFO just landed and posted this here

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

UFO just landed and posted this here
Тут главное не забывать, что при динамической типизации (диспетчеризация-то в runtime) для этого вообще «типы» могут и не понадобиться.

Например, можно вспомнить про guard'ы в erlang, какие-нибудь :-)
UFO just landed and posted this here
Они и при статической-то типизации нужны лишь для того, чтобы гарантировать валидированность данных. Не нужно её гарантировать — не используете всю эту наркоманию.
Тут есть один момент… что именно мы понимаем под «валидированностью данных»? Но речь не о нем… я лишь напомнил, что до какой-нибудь
f(K, L) when length(L) == K ->
...
невозможно будет «достучаться» без «массива длины, соответствующей этому числу». Т.е. это больше ответ на вопрос: «А как будет выглядеть?..»

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

Ровно как и при сильной динамической типизации — никто не запрещает получить весь набор «гарантий», что и при сильной статической.

Ну а то, что эти «гарантии» давать будет не компилятор… ну и что с того?! Это, имхо, вообще не принципиально.
UFO just landed and posted this here
Это как раз принципиально. Я хочу узнавать, что написал ерунду, так быстро, как можно, а не когда уже выкатил программу в прод.
?! А причем тут «прод»? Можно подумать, что динамическая типизация каким-то образом отменяет возможность статического анализа.
UFO just landed and posted this here
А если у меня есть статическая типизация, то зачем мне динамическая?
Динамическая типизация (если отбросить всякий субъективизм) нужна только для одного — нормальная множественная диспетчеризация.

Сугубое имхо, конечно :-)
UFO just landed and posted this here
А как статическая типизация мешает множественной диспетчеризации?
Очевидно, тем, что при организации диспетчеризации вынуждает оперировать исключительно типами «времени компиляции».

Т.е. диспетчеризация на основе типов «времени исполнения» (а это, собственно, и есть множественная диспетчеризация) — при статической типизации — может быть [хоть как-то] построена только «руками». И то, только в том случае, если доступна информация о типах «времени исполнения» (что есть далеко не везде).

То, что какие-то частные случаи (типа т.н. «двойной диспетчеризации») могут быть решены через т.н. «перегрузку» (там где эта самая «перегрузка» таки возможна) — ничего не меняет. В общем случае оно решается только при динамической типизации.
UFO just landed and posted this here
Я что-то не очень понимаю, чем вам не угодили, например, тайпклассы.
В смысле, кроме того, что как и при любой «перегрузке» всю диспетчеризацию надо расписывать ручками? :-)

Но — самом деле — я не могу сказать, что TC мне прям чем-то «не угодили»… TC, имхо, гораздо лучше чем, например, то что есть в каком-нибудь python'е (хотя, казалось бы...).

«Не угодила» тут как раз статическая типизация, которая приводит к тому, что малейшая «шероховатость» в диспетчеризации вываливается в необходимость выстраивать какую-либо иерархию типов… в haskell'е это конечно не так «больно», но всё равно это всё начинает «обрастать» вспомогательными типами, доп. TC и т.п.

А Вас не затруднит привести пример, когда в динамически типизированном языке что-то такое не требуется "расписывать ручками"?

А Вас не затруднит привести пример, когда в динамически типизированном языке что-то такое не требуется «расписывать ручками»?
Как простейший пример, можно представить ДКА со следующей «шероховатой» логикой:
1. В состоянии a любой сигнал приводит к переходу в состояние b;
2. Сигнал s для любого другого состояния не приводит к переходу состояния.

На erlang, например, это будет записано «так, как слышится»:
f({state, a}, {signal, _}) ->
  {{state, b}, []};
f(A = {state, _}, {signal, s}) ->
  {A, []};
...

При статической типизации, даже если у нас получается «вывести» две ф-ции: a -> AnySignal -> x и AnyState -> s -> x, то при попытке диспетчеризации варианта a -> s -> x получим неоднозначность, для разрешения которой и нужно будет его (этот вариант) *отдельно* «расписать».
Это не вопрос динамичности языка, а вопрос приоритета при разрешении перегрузок функций. Мне кажется более предпочтительным вариант, принятый большинством языков: от порядка записи перегрузок функции результат не зависит. У вас же, если я правильно понял, перестановка этих двух строк существенно меняет получаемую программу.
Это не вопрос динамичности языка, а вопрос приоритета при разрешении перегрузок функций.
Тут есть один нюанс. В примере на erlang вообще нет «перегрузок». Это *одна* функция :-)

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

Но диспетчеризация «перегрузок» — это не множественная диспетчеризация. Хотя и в чем-то похожа на.

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

Это, с одной стороны, немножечко наводит на мысли. А с другой — (сейчас подумал), что если в haskell добавить возможность указывать приоритет диспетчеризации при реализации классов типов (например, через какой-нибудь особый вид функциональной зависимости… типа как before в CLOS) — может получится сильно интересная вещь. Не сильно удивлюсь, кстати, если оно уже там есть… я все-таки — «не настоящий сварщик» :-)

У вас же, если я правильно понял, перестановка этих двух строк существенно меняет получаемую программу.
Поменяется конечно. Но дело-то вообще не в «порядке». Если мы переставим условия в задаче — неопределенность при диспетчеризации «перегрузок» никуда не денется. Она не «порядком» вызвана… она, скажем так, в том, что пересечение областей определения «перегружаемых» ф-ций — не пустое.
Про отличие перегрузок и множествественной диспатчеризации в эрланге тема не раскрыта. По моему пониманию, разница между ними просто в том, что перегрузка оперирует типами времени компиляции, а диспетчиризация — времени исполнения. По крайней мере основываясь на моём опыте работы с julia, где этот самый multiple dispatch.

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

...
add(a::Int, b) = a + promote_to_int(b)
add(a, b::Int) = promote_to_int(a) + b
...
add(a::Float, b) = a + promote_to_float(b)
add(a, b::Float) = promote_to_float(a) + b
...

x = 1 # int
y = 1.0 # float

add(x, y) # что выдаст?


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

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

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


На C# это можно тоже безо всякой диспетчеризации написать, и даже паттерн-матчинг не понадобится:


(State state, Action action) f(State s, Signal x)
{
    if (s == State.a)
        return (State.b, Action.None);

    if (x == Signal.s)
        return (s, Action.None);

    // ...
}

Уважаемый gBear, хочу напомнить, что статически типизированные ЯП не сводятся к C++, Java и C#.

Уважаемый gBear, хочу напомнить, что статически типизированные ЯП не сводятся к C++, Java и C#.
Дык и динамически типизированные не ограничиваются только js и python'ом каким-нибудь :-)
Ну в теории в языках вроде джавы/сишарпа вы можете дефинировать новый тип/класс/интерфейс, который «наружу» будет выглядеть именно так как описано выше.

Или я что-то неправильно понимаю в вопросе?
UFO just landed and posted this here
Что там запихать под капот это уже отдельный вопрос. И это на мой взгляд зависит от того для чего нам это надо.
А для нового интерфейса по идее можно вообще что угодно самому напридумывать.

П.С. У меня всё больше и больше впечатление что мы вроде как бы исползуем одни и теже термины, но говорим о немного разных вещах…
UFO just landed and posted this here
Вот даже не знаю как объяснить. То что вы написали это уже конкретная реализация, а я теперь уже пытаюсь придумать можно ли дефинировать контракт/интерфейс, который бы «работал» вне зависимости от того что там запихают в реализацию. Но похоже я просто что-то размечтался :)

А так в том же сишарпе вроде бы есть массивы/спискм с фиксированной длиной и можно попробовать как-то с ними поиграться.
UFO just landed and posted this here
ArrayList с FixedSize вроде бы можно и во время выполнения создавать.

UFO just landed and posted this here

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


var arr = new[] {1,2,3};
var arrWithFour = NewArrWithItem(arr, 4);
if (arr.Length == arrWithFour.Length) {
   ...
}

Вот в этом if (...) компилятор должен поругаться что expression is always false. Как там так типы написать, чтобы вызов NewArrWithItem дал нужную компилятору информацию?

Как вы думаете что произойдёт при выполнении вот этого куска кода:
 ArrayList myAL = new ArrayList();
 myAL.Add( "1" );
 myAL.Add( "2" );
 ArrayList fixedSizeAL = ArrayList.FixedSize(myAL);
 fixedSizeAL.Add("3");

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

А под константным массивом имелось в виду то, что такой код не скомпилируется с ошибкой «размеры массива неподходящие». Если дело дошло до рантайма — значит язык не справился с задачей.

И к слову, это не массив, а список. Что до IsFixedSize — это вообще адский костыль, как и IsReadOnly.

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


infix  4 :+:

data Vect : Nat -> Type -> Type where
   Nil  : Vect Z a
   (:+:) : a -> Vect k a -> Vect (S k) a

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


testFunc : (Vect n a) -> (Vect m a) -> (n = m) -> Integer
testFunc _ _ _ = 50

Давайте вызовем её с одинаком значением (вектор одной длины):


foo : Integer
foo = let arr = (1 :+: (2 :+: (3 :+: Nil)))
          arrWithFour = 4 :+: arr
      in testFunc arr arr Refl

всё работает. А теперь давайте попробуем передать туда arrWithFour:


Type checking .\./test.idr
.\./test.idr:15:10-38:
   |
15 |       in testFunc arr arrWithFour Refl
   |          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When checking right hand side of foo with expected type
        Integer

When checking an application of function Main.testFunc:
        Type mismatch between
                x = x (Type of Refl)
        and
                3 = 4 (Expected type)

        Specifically:
                Type mismatch between
                        0
                and
                        1

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




Можно возразить что идрис такой умный, но нет, даже паскаль которму 50 лет умеет проверять размеры массива:


@bq:00:46:44:/tmp/dl$ fpc -Cr a.pas
Free Pascal Compiler version 3.0.4+dfsg-23 [2019/11/25] for x86_64
Copyright (c) 1993-2017 by Florian Klaempfl and others
Target OS: Linux for x86-64
Compiling a.pas
a.pas(20,5) Error: range check error while evaluating constants (0 must be between 1 and 5)
a.pas(32,5) Error: range check error while evaluating constants (55 must be between 1 and 5)
a.pas(44) Fatal: There were 2 errors compiling module, stopping
Fatal: Compilation aborted
Error: /usr/bin/ppcx64 returned an error exitcode

Пусть это и не так круто, что делает идрис, но это хоть что-то. В сишарпе же написать (new int[0])[100500]
и получить Index was outside the bounds of the array в рантайме нет никаких проблем.

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

Подозреваю, что поэтому у вас MyPy не взлетел — ваш подход к решению задач совсем иной.

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

У меня обычно если такое требуется передавать между слоями, там интуитивно напрашивается сущность/таблица сущностей. Ну то есть, в теории можно конечно обмазываться аннотациями типа «6 строк длинною 2 через символ :», но по-моему проще сделать сущность MacAddress и доверять ей на основе валидации в конструкторе, или еще как. И да, это разумеется никак не компенсирует отсутствие зависимых типов. Если их подвезут в питон, я в принципе не буду против, и возможно даже где-то буду их применять (в адаптерах например)
UFO just landed and posted this here
Нет, не приходится. Уже давно есть аннотации, которые решают вашу проблему на этапе написания, а не рантайма.

То есть мы тратим время на проверку типов и до написания программы, и во время её исполнения? Прям win-win какой-то.

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

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

А может наоборот меньше :)

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

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

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

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

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

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

А зачем? Извините, без наезда, но, похоже, вы просто слишком упоролись по статике.
Функции, обычно, нужно, чтобы её аргумент квакал как утка и выглядел как утка. А что там у него внутри — живая утка, Скрудж МакДак, уточка для ванны или уткоподобный робот-убийца — функцию не интересует и, в общем, не должно интересовать для уменьшения связности.
Ладно бы ещё статическая проверка типов действительно предотвращала большой процент ошибок. Но она совсем не всесильна. Выше в комментах я вас спрашивал про фазу Луны, которую вы аккуратно «забыли»; не зря спрашивал, потому что эта информация недоступна на этапе компиляции и не может быть, соответственно, проверена системой типов.
А зачем? Извините, без наезда, но, похоже, вы просто слишком упоролись по статике.

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


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

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


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

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

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

Умножение килограммов на мандарины это проблема слабой типизации и неявных преобразований, а не проблема динамической типизации как таковой. В JavaScript можно сделать [] * "abcdef", но Python этого не разрешит.

UFO just landed and posted this here
Python этого не разрешит

Тут интересный вопрос, что означает это слово.


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

К сожалению, в 2020 году, когда машины распознают дорожную разметку и пешеходов, IDE всё ещё не способны распознать тип переменной в программе.

Все дело в производительности — IDE должна работать без лагов, реактивно реагировать на действия пользователя.

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

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

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


Так что вынужден не согласиться: данная стадия компиляции или существует, или нет. И типизация тоже существует. :-)

Если уж быть совсем точным, то Теренс У. Пратт недаром в своём учебнике 1970-х (который при последующих переизданиях вырос в объёме раза в три, но в стройности изложения, увы, понёс потери) ввёл понятие «время связывания». Которое, когда говорят о языках и средах программирования прежде всего касатся типизации и распределения памяти, но не только. Compiletime и runtime — два возможных значения. Даже не крайних, то есть runtime то наверное — крайняя точка, но помимо времени компиляции есть ещё время проектирования языка, компилятора и железа, на котором они будут работать.

Ну а так, да, если не вдаваться во все «оттенки серого»: compileteme — статика, runtime — динамика.

До того как появился язык ML (ныне в основном известны такие его диалекты, как Ocaml и F#, ну и Haskell тоже кое-что оттуда унаследовал) и вывод типов не пошёл в мейнстрим, считалось, что языки со статической типизацией требуют многословия и неудобны. Сегодня это, очевидно, не совсем так. И традиционно динамические языки, начиная с первого из них — лиспа (см. SBCL), начали миграцию в сторону статики. Python, в общем-то тоже. Что, кстати, неудивительно, с учётом того, что те библиотеки, которые во многом и делают Питон таким популярным, написаны фактически (в примерно равных частях) на C, C++ и Фортране (и немножко питона, чтоб приклеилось).

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

Интересно а существует ли язык который с одной стороны будет максимально строгим (то есть гарантировать отсутствие рантайм-ошибок) а с другой стороны в нем не будет никакого синтаксиса объявления типов так как он будет выводить и проверять все типы из кода?


Почему без объявления типов?

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

Я если честно совсем не уверен что существуют языки, которые удовлетворяют хотя бы первой части ваших требований :)
Возможность указать тип имеет тенденцию усложняться — просто указать тип недостаточно надо еще добавить возможность generic-типов а потом потребуется добавить контр- и ковариантность и различные ограничений по иерархии типов а потом возможность объединения-пересечения типов, но потом этого тоже будет недостаточно и появится желание добавить conditional-типы как в последнем тайпскрипте, а потом всякие параметризированные или зависимые типы как в julia или idris — и в итоге внутри языка мы создаем еще один язык со своим синтаксисом но уже на типах


Вот, абсолютно верно. (С)

Типизация — это собственный МОНСТР, который будет всё усложняться и усложняться, пока НЕ разделит судьбу динозавров, типа CODASYL или Java EE, да и OOП.
с одной стороны будет максимально строгим (то есть гарантировать отсутствие рантайм-ошибок)
Если в число рантайм-ошибок включать зависание, то все Тьюринг-полные языки не подходят. А прочие ошибки (как-то деление на ноль) лёгким движением руки можно превратить в зависание, лишь немного изменив семантику языка.
Правды ради стоит отметить, что на практике нам не всегда нужны Тьюринг-полные ЯП.
в нем не будет никакого синтаксиса объявления типов так как он будет выводить и проверять все типы из кода?
Первые исследования систем типов проводились на лямбда-исчислении. А.Черч предложил (явную) простую систему типов, в которой тип был неотъемлемой частью функций. Х.Карри предложил эквивалентную (неявную) систему, в которой тип как языковая конструкция вводился только на этапе вывода, но не был синтаксическим элементом языка.
Так что да, существовать могут.
Общая идея неявных систем типов состоит в том, что тип возникает только на этапе проверки (вывода) типов подобно тому, как нетерминальные символы возникают только при описании грамматики/синтаксическом разборе, но не являются частью языка.
Как бы это странно не звучало, но в рамках подхода Карри JavaScript со сборщиком, который падает, если для каждой переменной не может быть однозначно выведен тип, вполне может считаться языком с неявной типизацией.
Возможность указать тип имеет тенденцию усложняться… в итоге внутри языка мы создаем еще один язык со своим синтаксисом но уже на типах
Тенденция усложняться связана не с возможностью указать тип, а с желаниями иметь мощную систему типов, позволяющая гибче абстрагироваться, сохраняя при этом строгость.
Если система типов явная, то да — язык типов в языке, так и задумано.
Если система типов неявная, то язык типов всё равно будет сложный, но сокрытый внутри тайпчекера.
Из плюсов: 1) текст станет чище от типов, 2) пользователю не нужно знать язык типов.
Из минусов: 1) невозможно передать тайпчекеру информацию относительно типов или их вывода (всё усугубляется требованием к тайпчекеру, чтобы он выводил типы за конечное время), 2) невозможно использовать типы в качестве схемы/интерфейса или как способ описания фрагмента предметной области.

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

"Направление? Какое направление?"


Гибридный подход в научной среде называется gradual typing, Matthias Felleisen занимается исследованиями в этой области кучу лет (10+) и он пришёл к выводу, что это дохлое направление.
https://www.youtube.com/watch?v=5DlEj6daNEo
https://www.ccs.neu.edu/home/asumu/slides/popl-2016-01-21.pdf
https://www2.ccs.neu.edu/racket/pubs/popl16-tfgnvf.pdf

Я говорил не только про gradual typing, хотя этот подход является частным случаем.

Общая задача gradual typing и иже с ним (напр., threesomes) пытается сдружить типизированный код и нетипизированный. Грубо говоря, gradual typing предоставляет возможность локально положить болт на надёжную (sound) систему типов, чтобы затем проверять контракты в рантайме. По моим представлениям это исходно порочный подход, если только не в связи с задачей миграции, о которой говорится в лекции.

Я же говорил о том, чтобы использоваться явную систему типизации (т.е. ту, в которой тип является частью грамматики языка), которая позволяет не писать типы, если в этом нет необходимости (в противовес всем системам типов лямбда-куба, где типы необходимо указывать для каждой переменной). Как пример, в Java появился var, в Haskell изначально был Хиндли-Милнер.

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

P.S. спасибо за ссылки.

upd. а мог бы оставить ссылку на ту же мысль, выраженную другими словами.
«Typing objects in TypeScript».

«Объекты типа Object (с заглавной буквы „О“) — это все инстансы класса Object.

Объекты типы object (со строчной буквы „o“) — представляют собой все непримитивные значения.

При этом тип Object включает примитивные значения:

function f(x: Object) { }
f('abc'); // OK

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

Kanut
и в итоге внутри языка мы создаем еще один язык со своим синтаксисом но уже на типах


Типизация — это собственный МОНСТР. ©

Articles