Нет, динамические системы типов по своей сути не более открыты

Автор оригинала: Alexis King
  • Перевод

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



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


Две лжи о типах


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


Решительно не согласен с постом […], который продвигает совершенно запутанный и статичный взгляд на мир. Предполагается, что мы можем или должны теоретически установить, что именно является «допустимым» вводом на границе между программой и миром, таким образом привнося ощущение сильной связности на всю систему, и тогда несоответствие какой-либо схеме автоматически приводит к сбою в работе программы.
Здесь это рекламируется как полезное свойство, но представьте, если бы интернет работал бы таким образом. Сервер поменял свой JSON ответ, и нам нужно теперь перекомпилировать и перепрограммировать весь интернет. Это статическое представление, которое продвигается как полезная вещь. […] «Менталитет парсинга» является принципиально жестким и глобальным, в то время как отказоустойчивая система должна проектироваться как децентрализованная и предоставлять интерпретацию данных получателю.

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


Второй комментарий был оставлен на Hacker News и он значительно короче первого:


Какой будет сигнатура, скажем, питоновского pickle.load()?

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


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


Вы не можете обработать то, что вы не знаете


Утверждение простое: в статической системе типов вы должны заранее объявить схему данных, но в динамической системе типов такой тип может быть, ну, в общем, динамическим! Это звучит как само собой разумеющееся настолько, что Рич Хикки практически построил свою карьеру оратора на эмоциональной привлекательности данного тезиса. Единственная проблема в том, что он не верен.


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


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


{
  "event_type": "signup",
  "timestamp": "2020-01-19T05:37:09Z",
  "data": {
    "user": {
      "id": 42,
      "name": "Alyssa",
      "email": "alyssa@example.com"
    }
  }
}

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


const handleEvent = ({ event_type, data }) => {
  switch (event_type) {
    case 'login':
      /* ... */
      break
    case 'signup':
      sendEmail(data.user.email, `Welcome to Blockchain Emporium, ${data.user.name}!`)
      break
  }
}

Но что, если этот сервис будет написан на Haskell? Будучи прилежными, ожидающими недоброе от реального мира программистами на Haskell, которые парсят, а не валидируют, мы можем написать такой код:


data Event = Login LoginPayload | Signup SignupPayload
data LoginPayload = LoginPayload { userId :: Int }
data SignupPayload = SignupPayload
  { userId :: Int
  , userName :: Text
  , userEmail :: Text
  }

instance FromJSON Event where
  parseJSON = withObject "Event" \obj -> do
    eventType <- obj .: "event_type"
    case eventType of
      "login" -> Login <$> (obj .: "data")
      "signup" -> Signup <$> (obj .: "signup")
      _ -> fail $ "unknown event_type: " <> eventType

instance FromJSON LoginPayload where { ... }
instance FromJSON SignupPayload where { ... }

handleEvent :: JSON.Value -> IO ()
handleEvent payload = case fromJSON payload of
  Success (Login LoginPayload { userId }) -> {- ... -}
  Success (Signup SignupPayload { userName, userEmail }) ->
    sendEmail userEmail $ "Welcome to Blockchain Emporium, " <> userName <> "!"
  Error message -> fail $ "could not parse event: " <> message

Этот фрагмент несомненно более многословен, хотя ожидать некоторых дополнительных определений типов вполне естественно (и, да, они выглядят сильно преувеличенно в таких крошечных примерах). Однако обсуждаемые нами аргументы в любом случае не относятся к размеру кода. Настоящая проблема с этой версией кода, согласно предыдущему комментарию на Reddit, заключается в том, что код на Haskell должен быть обновлён всякий раз, когда сервис входа добавляет новый тип события! Новый конструктор типа должен быть добавлен к типу данных Event, и для него должна быть определена новая логика парсинга. А что будет, когда новые поля будут добавлены в полезную нагрузку? Какой кошмар для поддержки.


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


За исключением того, что нет, это не так. Если мы не обновляем тип Event, то единственная причина сбоя статически типизированной программы заключается в том, что мы именно так и написали функцию handleEvent. Мы могли бы просто сделать то же самое в коде JavaScript, добавив случай по умолчанию, который отвергает неизвестные типы событий:


const handleEvent = ({ event_type, data }) => {
  switch (event_type) {
    /* ... */
    default:
      throw new Error(`unknown event_type: ${event_type}`)
  }
}

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


handleEvent :: JSON.Value -> IO ()
handleEvent payload = case fromJSON payload of
  {- ... -}
  Error _ -> pure ()

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


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


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


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


  • Более того, оказывается, что код Haskell на самом деле не использует поле userId в типе SignupPayload, поэтому этот тип слишком консервативен. Если мы хотим убедиться, что оно действительно не нужно (поскольку, возможно, мы постепенно избавляемся от появления userId в этой полезной нагрузке), нам нужно только удалить это поле записи; если проверка типов проходит, ура, мы можем быть уверены, что код действительно не зависит от этого поля.


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



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


Приведенный выше код JavaScript делает все те же предположения, что и наш код на Haskell: он предполагает, что полезные нагрузки событий являются объектами JSON с полем event_type и для событий «типа» signup включают в себя поля data.user.name и data.user.email. Он не может сделать ничего полезного с действительно неизвестными входными данными! Если добавляется новый тип события, наш код JavaScript не может магически адаптироваться к нему лишь потому, что он динамически типизирован. Динамическая типизация просто означает, что типы значений переносятся вместе с ними на время выполнения и проверяются по мере выполнения программы; типы все еще там, и эта программа все еще неявно полагается на то, что обрабатываемые значения являются какими-то конкретными.


Сохранение непрозрачности данных


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


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


const handleEvent = (payload) => {
  const signedPayload = { ...payload, signature: signature(payload) }
  retransmitEvent(signedPayload)
}

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


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


handleEvent :: JSON.Value -> IO ()
handleEvent (Object payload) = do
  let signedPayload = Map.insert "signature" (signature payload) payload
  retransmitEvent signedPayload
handleEvent payload = fail $ "event payload was not an object " <> show payload

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


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


Еще раз отметим, что то предположение, которое мы были вынуждены сделать явным в коде на Haskell, также сделано и кодом на JavaScript! Если бы наша функция JavaScript handleEvent была вызвана со строкой (а она тоже является JSON значением), а не с объектом, весьма маловероятно, что поведение будет желательным, поскольку построение объекта по строке через spread-оператор приводит к следующему сюрпризу:


> { ..."payload", signature: "sig" }
{0: "p", 1: "a", 2: "y", 3: "l", 4: "o", 5: "a", 6: "d", signature: "sig"}

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




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


type UserId = UUID

Тем не менее, наш комментатор Reddit, вероятно, будет гнобить нас за это! Если в спецификации на API явно не указано, что все идентификаторы пользователей будут UUID, то это делая такое предположение, мы выходим за определённые границы. Хотя идентификаторы пользователей могут быть UUID сегодня, возможно, они не будут таковыми завтра, и тогда наш код сломается без причины! Это же вина статической типизации, не так ли?


Опять же, ответ — нет. Это случай неправильного моделирования данных, но статическая система типов не виновата — её просто неправильно использовали. На самом деле, наиболее подходящий способ представления UserId в данном случае — это определить новый непрозрачный тип:


newtype UserId = UserId Text
  deriving (Eq, FromJSON, ToJSON)

В отличие от определённого выше псевдонима типа, который просто создаёт новое имя для существующего типа UUID, новое объявление создает совершенно новый тип UserId, который отличается от всех других типов, включая Text. Если мы оставим конструктор типа данных закрытым (то есть не будем экспортировать модуля, который определяет этот тип), то единственный способ создать UserId — это распарсить его с помощью FromJSON. Продолжая дальше, единственное, что вы можете сделать с UserId — это сравнить его с другими UserId для равенства или сериализовать его с помощью ToJSON. Больше ничего не разрешено: система типов не позволит вам зависеть от внутреннего представления идентификаторов пользователей в чужом сервисе.


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


Замечание 1

Технически, вы можете злоупотребить классом типов FromJSON для преобразования произвольной строки в UserId, но это будет не так прямолинейно, как кажется, так как fromJSON может отвергнуть входные данные. Это означает, что вам каким-то образом придётся справиться с этим случаем отказа. Поэтому данный трюк вряд ли продвинет вас слишком далеко, если вы уже не находитесь в том контексте, где вы выполняете парсинг ввода… но в таком случае было бы легче просто выполнить преобразование уже разобранного ввода в UserId. Так что система типов не мешает вам делать все возможное, чтобы выстрелить себе в ногу, но она направляет вас к правильному решению (и да, не существует никакой технологии, которая могла бы гарантированно защитить программистов от превращения их собственной жизни в кошмар, если они намерены это сделать).


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


Рефлексия не является чем-то особенным


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


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


Однако оказывается, что эта ситуация фактически идентична предыдущим примерам с использованием JSON, и тот факт, что библиотека pickle из Python сериализует нативные объекты Python напрямую, ничего не меняет. Почему? Давайте рассмотрим, что происходит после того, как программа вызывает pickle.load(). Допустим, вы пишете следующую функцию:


def load_value(f):
  val = pickle.load(f)
  # сделать что-нибудь с `val`

Проблема в том, что теперь val может быть любого типа, и так же, как вы не можете делать ничего полезного с действительно неизвестным, неструктурированным вводом, вы не можете ничего сделать со значением, если не знаете хотя бы что-нибудь о нём. Если вы вызываете какой-либо метод или обращаетесь к какому-либо полю в результате, то вы уже сделали предположение о том, что же на самом деле pickle.load(f) вернёт и, оказывается, что эти предположения относятся к типу val!


Например, представьте, что единственное, что вы делаете с val это вызываете val.foo() и возвращаете результат вызова, который ожидается будет строкой. Если бы мы писали Java, то ожидаемый тип val был бы довольно простым — мы бы ожидали, что он будет экземпляром следующего интерфейса:


interface Foo extends Serializable {
  String foo();
}

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


static <T extends Serializable> Optional<T> load(InputStream in, Class<? extends T> cls);

Зануды конечно будут придираться к тому, что это не то же самое, что pickle.load(), так как вы должны передать токен Class<T> чтобы заблаговременно выбрать тип. Однако ничто не мешает вам передавать Serializable.class и преобразовывать к нужному типу позже, после загрузки объекта. И это ключевой момент: в тот момент, когда вы делаете с объектом хоть что-то, вы должны знать что-то о его типе, даже на динамически типизированном языке! Язык со статической типизацией просто заставляет вас быть более явным, как это делалось, когда мы говорили о полезных нагрузках в JSON-сообщениях.




Можем ли мы сделать подобную типизацию и в Haskell? Абсолютно точно — мы можем использовать библиотеку serialise, которая имеет API, аналогичный такому, как в Java, который упомянут выше. Этот API имеет интерфейс, очень похожий на библиотеку Haskell для работы с JSON, aeson, поскольку, как оказывается, проблема работы с неизвестными данными JSON не сильно отличается от работы с любым неизвестным значением в Haskell — в какой-то момент вы должны выполнить какой-то разбор, чтобы сделать с полученным значением что-нибудь.


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


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


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


Приложение: реальность, скрытая за мифами


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


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


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


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


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


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


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


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



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


Если вы заинтересованы в изучении систем статических типов с сильной поддержкой структурной типизации, я бы порекомендовала взглянуть на любой из следующих языков: TypeScript, Flow, PureScript, Elm, OCaml или Reason, каждый из которых имеет некоторую поддержку структурно типизированных записей. Чего бы я не рекомендовала для этой цели — так это Haskell, который имеет ужасную поддержку структурной типизации; Haskell (по разным причинам, выходящим за рамки этой статьи) агрессивно номинативен.


Замечание 2

Я считаю, что это самый существенный недостаток Haskell на момент написания этой статьи.


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


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

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 432

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

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

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

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

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

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

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

          Я как-то писал код на солидити, и писал там связный список индексов массива (у меня был массив, а сбоку мне нужно было хранить порядок обхода элементов чтобы они были отсортированны по одному из полей). То есть у меня был 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;
              }
          }

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

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

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


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

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

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

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

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

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

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


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

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

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


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

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

                          0
                          обычно

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

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


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


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


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


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


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

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

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

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


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

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

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


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

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

                    0
                    Да, вот явный каст и есть плохо: получается, мы не используем типы в типизированном языке в полной мере. В 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;
                    

                +7
                Есть еще преимущества например типизированной джавы vs джаваскрипт. Допутим у нас очень простой веб-сервис, мы на джаваскрипте анписали логику за час, на джаве-за полтора.
                Но после создания веб-сервиса на спринге у меня сваггер будет сгенерирован автоматически из указанных мной типов, валидация входящих параметров также появится сама собой.
                На джаваскрипте мне придется или добавлять те же типы и из них генерить сваггер или руками самому держать сваггер в акутальном состоянии. Валидация делается обычно также отдельно, например используется joi, для которого по сути я указываю те же типы еще раз, но в другом формате. И после этого мне нужны тесты чтобы быть уверенным что возвращаемые данные всегда соотвствуют выходному парметру-в джаве же в этом я буду уверен и так.
                По факту у меня не выходит написание микросервсиов готовых к выходу на продакшен на джаваскрипте сильно быстрее. И это при том что в джаве все-таки не самая сильная система типов.
                  0
                  Статической типизации избегают либо трусливые параноики («я не знаю что поменяется завтра, надо сделать яп внутри яп нафигачить всё динамически»), либо люди, которые не могут в формализацию.
                    –1
                    … или просто не любят писать слишком много лишнего кода.
                      +4
                      на такой случай существует type deduction и implicit conversion
                        +2
                        Вывод типов позсоляет лишнего кода почти не писать, а полиморфизм писать кода даже меньше, чем в большенстве динамически типизированных языках (ну может кроме Julia и Common Lisp).
                        К тому же IDE часто позволяет генерировать код исходя из информации о типах.
                          +1
                          Полиморфизм никак не связан со строгостью системы типов. И да, для динамически типизированных языков тоже придумали IDE с автогенерацией кода.
                            +1
                            Полиморфизм никак не связан со строгостью системы типов.

                            Это скорее эмпирическое наблюдение: почему-то наличие параметрического полиморфизма оказывается скоррелированным со строгостью системы типов.

                              +1
                              Для полиморфизма по параметризованным типам (например монадам) требуются параметризованные типы, из динамических языков я такие знаю только в Julia (в CL можно сделать, но я не видел и сам не пробовал).
                              Современные IDE пытаются изобразить статически типизированный язык из динамически типизированного, прогоняя вывод типов. Но информации для полноценной реализации Type-Driven Development у них просто нет.
                                +6

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                            Что в общем-то означает что магия это технология, которая просто слишком развита для понимания тем, кто считает её магией. Так что как по мне то никакой магии нам в информатике не надо :)
                                          +4

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

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

                                        Можно пример таких задач?

                                          –1
                                          Распарсить мегабайтный json или xml, поменять там пару строк и запарсить обратно. Сейчас у нас везде микросервисы в облаках, которые не должны знать друг о друге. А с точки зрения юзера это всё ещё один документ (файл). В котором вы знаете свой селектор (не путь).
                                            +3
                                            И почему это по вашему мнению в статическом виде невозможно или прям таки сильно затратнее? Ну то есть в чём конкретно проблема должна заключаться?
                                              +4

                                              Легче лёгкого:



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

                                                0

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


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

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

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

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


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

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

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


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

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


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

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

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

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

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


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

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

                                                                0

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

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

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

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

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

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

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

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

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

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

                                                              0

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


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

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

                                                                  0

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

                                                                    +1

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

                                                                    0

                                                                    К счастью у меня под рукой как раз оказалось Generic решение такой задачи для JSON'а: https://gist.github.com/Nexmean/2a85607888c6af2bfa0ec9116847ba71


                                                                    Примерно аналогичным образом это можно сделать для XML.

                                                                      0

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

                                                                        0

                                                                        del, неправильно понял контекст

                                                                +1

                                                                Ну потому что у вас функция Element врёт. Она говорит, что возвращает элемент, а должна возвращать либо Just элемент, либо Nothing.


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

                                                                  0

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

                                                                    –1

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

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

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

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

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


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

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

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

                                                                      0

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

                                                                        0

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

                                                                          0

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

                                                            +1

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

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

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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


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


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

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

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


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

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


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

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

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

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


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

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

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


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

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

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

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


                                                              Удобно, ничего не скажешь.

                                                                –4
                                                                индуктивно гарантирует корректность всей программы

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

                                                                  +3

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


                                                                  Серьёзно, ни разу не получал там сегфолты. И unexpected type exception не получал (более того, даже не знаю, что это такое).

                                                                    +4

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

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

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

                                                                        +1

                                                                        А, так вот оно зачем нужно!

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

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

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

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

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

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

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

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

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

                                                                          Да ну?

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

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

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

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

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

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

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

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


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

                                                                          +5

                                                                          Медленно в рантайме? Совсем не факт.


                                                                          Отсутствие статической типизации не означает просаживание в скорости — вспомните о существовании ассемблера, в конце концов.

                                                                            0

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




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

                                                                              +2

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


                                                                              Скрытый текст

                                                                              Даже наоборот — выразил вот прям сейчас в соседнем окне кое-что через GADT для пущей безопасности и потерял процентов 30% производительности, так как компилятор больше не может чего-то заинлайнить. Приходится смотреть на GHC Core на ночь глядя.

                                                                              +2

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


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


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


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


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


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

                                                                                –4

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

                                                                                  +5

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


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

                                                                                    +3

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


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

                                                                                      +1

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


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

                                                                                        0

                                                                                        «Это нормально. Но неправильно.»

                                                                                          +1
                                                                                          > Деструкторы для 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, пусть он даже тупо копирует поля — уже нормально, есть парная деструкция. Но почему??
                                                                                            0
                                                                                            Вызываются, если соответствующие конструирования уже были завершены в конструкторе

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

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

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

                                                                                                  +1

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

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

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

                                                                                                      0
                                                                                                      Там кажется компилятор слишком вольно реализуют aggregate initialization, если явно задать конструктор, то деструктор у lock'а зовётся там где ожидается. В целом да, выглядит как баг в gcc. AFAIK, в gcc про это (evaluation order и т.д.) достаточно багов было.
                                                                                              +3
                                                                                              Не совсем корректный пример с ассемблером. Там вообще нет типизации. Поэтому, строго говоря, для производительности вообще типизация не нужна, ни статическая, ни динамическая.

                                                                                              Именно. И это утверждение совместно с тезисом «статическая типизация для производительности не обязательна».


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

                                                                                              Не нужно, если вы как-то что-то доказали снаружи.


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


                                                                                              Вообще, пишите на С++ :)

                                                                                              Не, я чё-т устал. Как раз в черновиках уже ваяю статью-нытьё на тему.


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

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

                                                                                                0
                                                                                                И это утверждение совместно с тезисом «статическая типизация для производительности не обязательна».

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


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

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


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

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


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

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

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

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

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

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

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

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

                                                                                                      +3

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

                                                                                                        0

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

                                                                                                      0
                                                                                                      Generics? Templates?
                                                                                                    +8

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


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

                                                                                                      –1
                                                                                                      ну, когда API отдает по одному URL разные типы объектов — этот не REST
                                                                                                        0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                                                                                  0

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

                                                                                                                                    +2

                                                                                                                                    Как часто?


                                                                                                                                    Динамическую систему типов (неважно, что это оксюморон, ну да ладно) я реализовывал единожды, когда было интересно, что получится из статического типа (ty : Type ** val : ty) (не получилось, кстати, ничего, но это другой разговор). Встречался с чем-то таким я вообще ноль раз.


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


                                                                                                                                    Самое близкое к динамической типизации, что я делал — это, но и там на самом деле статика с рантайм-проверкой тега, не более.

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

                                                                                                                              Го по сути является языком, который сделан для людей которые пишут на динамических языках, и сам не особо статический, любую нетривиальную логику надо писать как 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 я бы не стал.

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

                                                                                                                                написали cty,

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

                                                                                                                    Not Invented Here