Священные войны в интернете о системах типов по-прежнему страдают от распространенного мифа о том, что динамические системы типов по своей природе лучше подходят для моделирования предметных областей «открытого мира». Обычно аргумент звучит так: цель статической типизации состоит в том, чтобы как можно более точно зафиксировать все сущности, однако в реальном мире это просто неудобно. Реальные системы должны быть слабо связаны и должны как можно меньше быть завязаны на представление данных, поэтому динамическая типизация приводит к более устойчивой системе в целом.
Эта история звучит убедительно, однако это неправда. Ошибка в таких рассуждениях заключается в том, что статические типы не предназначены для «классификации мира» или определения структуры каждого значения в системе. Реальность такова, что статические системы типов позволяют точно указать, сколько именно компонент должен знать о структуре его входных данных, и, наоборот, сколько он не знает. На практике статические системы типов превосходно обрабатывают данные с частично известной структурой, равно как и имеют возможность сделать так, чтобы логика приложения случайно не предполагала слишком многого о данных.
Две лжи о типах
Я давно уже хотела написать статью на эту тему в блоге, но последним толчком для этого решения были дезинформирующие комментарии в ответ на мою предыдущую статью. В частности, два комментария особенно привлекли мое внимание, первый из которых был опубликован на /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
из произвольной строки.
Технически, вы можете злоупотребить классом типов 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).
Это одна из главных мыслей, обсуждаемых Ричем Хикки во многих своих выступлениях, в которых критикуется статическая типизация. Он выдвинул идею, что эта способность играючи объединять, разделять и преобразовывать записи делает динамическую типизацию особенно подходящей для области распределенных, открытых систем. К сожалению, эта риторика имеет два существенных пробела:
Он слишком близок к тому, чтобы называть это фундаментальным ограничением систем типов, предполагая, что не просто неудобно, но и якобы невозможно моделировать такие системы в номинативной статической системе типов. Это не только не соответствует действительности (как продемонстрировано в данной статье), но и дезориентирует людей с точки зрения действительно ценного качества: практического, прагматического преимущества более структурированного подхода к моделированию данных.
Он путает отличие между структурным и номинативным с отличием между динамическим и статическим, создавая ошибочное представление того, что лёгкое объединение и разделение записей представленных в виде пар ключ-значение возможно только в динамически типизированном языке. Собственно, является фактом не только то, что языки со статической типизацией поддерживают структурную типизацию, но и многие языки с динамической типизацией также поддерживают номинативную типизацию. Исторически эти оси имеют некоторую корреляцию, но теоретически они ортогональны.
Для контрпримера к этим утверждениям рассмотрим классы Python, которые являются вполне номинативными, несмотря на то, что они динамические, и интерфейсы TypeScript, которые являются структурными, хотя и определяются статически. Действительно, современные статически типизированные языки все чаще получают встроенную поддержку структурно типизированных записей. В этих системах типы записей работают подобно хэш-таблицам в Clojure — они не являются отдельными именованными типами, а представляют собой анонимные коллекции пар ключ-значение — и они поддерживают многие из тех же выразительных манипуляций, что и хэш-таблицы Clojure, и всё в статически-типизированных рамках.
Если вы заинтересованы в изучении систем статических типов с сильной поддержкой структурной типизации, я бы порекомендовала взглянуть на любой из следующих языков: TypeScript, Flow, PureScript, Elm, OCaml или Reason, каждый из которых имеет некоторую поддержку структурно типизированных записей. Чего бы я не рекомендовала для этой цели — так это Haskell, который имеет ужасную поддержку структурной типизации; Haskell (по разным причинам, выходящим за рамки этой статьи) агрессивно номинативен.
Я считаю, что это самый существенный недостаток Haskell на момент написания этой статьи.
Означает ли это, что Haskell плох, или что его практически невозможно использовать для решения подобных проблем? Нет, конечно нет; есть много способов смоделировать эти решения в Haskell, и работают они достаточно хорошо, хотя некоторые из них страдают от значительного количества рутинного кода. Основной тезис этой статьи относится как к Haskell, так и к любому из других языков, упомянутых выше. Тем не менее, было бы упущением не упоминать об этой особенности Haskell, поскольку это может дать приверженцам динамической типизации, которые исторически находили статически типизированные языки гораздо более раздражающими, более ясное понимание настоящей причины таких ощущений. (В принципе, все основные статически типизированные ООП языки ещё более номинативны, чем Haskell!)
В качестве заключительной мысли: эта статья не предназначена для того, чтобы начать священную войну, равно как и не предназначена, чтобы атаковать динамическую типизацию. Существует много решений в динамически типизированных языках, которые действительно трудно перевести в статически типизированный контекст, и я думаю, что как раз обсуждение этих решений может быть продуктивным. Цель этой статьи — объяснить, почему одно конкретное рассуждение является тупиковым, поэтому, пожалуйста, прекратите приводить эти аргументы. Есть гораздо более плодотворные способы вести диалог о типизации.
AlexSpaizNet
Я уже давно перестал влезать в эти разборки. Для меня важнее что бы человек понимал, что на питоне, жаваскрипте и т.п. он напишет и поднимет сервис быстрее чем не той же жаве или даже го, и кода меньше будет в разы… но когда ему придется это код поддерживать и рефакторить, особенно когда по нему прошлись десятки разработчиков — да поможет ему бог.
justboris
А для меня важнее то, что бы когда человек начал писать какой-то сервис, он довел работу до конца, а не бесконечно сновал между разными языками, так ничего и не произведя.
LonelyDeveloper97
А действительно ли быстрее?
Если не рассматривать вебсервис формата «отдай html страницу», а хоть какую-то бизнес-логику?
Статические проверки типов — отличный пример fail-fast концепции.
Ты захотел вызвать функцию из библиотеки и передал туда то, что нельзя? — Ты узнал об этом как только написал код.
Ты хочешь понять из чего состоит ответ от сервера? Ты полностью видишь его структуру и понимаешь что можно и что нельзя с ним делать.
Возможно я просто не умею писать на динамических ЯП, но у меня ни разу не получалось сделать так, чтобы то, что я пишу — заработало без ошибок. Если это не print(1+10).
Создать маленький скрипт, строк на 20, чтобы нейросеточку обучить или там данные в R обработать и график вывести — это ок. Написать, ну хотя-бы нормальный калькулятор, так чтобы новые функции туда вводить парой строчек кода — уже мучения с отладкой и вопросы небу «Что я сделал не так»?
При этом в статическом ЯП я просто пишу и компилирую. И очень часто все работает как ожидалось. IDE отсекает за меня целый класс ошибок, просто не давая мне их сделать. По моему это прекрасно)
PS
А большая часть ошибок возникает там, где были приняты решения формата: «Нет, мы не будем создавать тип для этого перечисления, мы запихаем сюда Int и будем проверять на этапе исполнения».
PsyHaSTe
Я как-то писал код на солидити, и писал там связный список индексов массива (у меня был массив, а сбоку мне нужно было хранить порядок обхода элементов чтобы они были отсортированны по одному из полей). То есть у меня был 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 строчек кода, но так и не нашел в чем проблема. Для желающих, можете сами поискать баг, в коде:Промучился я эти два дня… А потом за полчаса зачинил. Как? А вот так:
В итоге я до сих пор до конца точно не знаю, в чем была проблема, но типчики помогли мне её побороть.
mvv-rus
Ну, я код ваш не отлаживал, потому за причину ошибки точно сказать не могу.
Но мне сразу бросилось в глаза некорректное сравнение в последнем if-then-else:
if (node.next > 0) {
0 — это вполне допустимое значение для ссылки на следующий элемент в цепочке, а код написан так, как будто 0 — пустое значение. Каковы последствия этой ошибки — я не анализировал, потому сказать, та ли это ошибка была или нет (и не было ли других), я не могу.
Естественно, использование явной проверки на непустое значение с помощью типчиков исправило эту ошибку. Но букв для этого вам написать пришлось заметно больше, не правда ли?
PS Однако экономия 2-х дней и потраченных за это время нервов лично для вас очевидно была целесообразной, не спорю.
PsyHaSTe
Насколько я помню, это отличие я тоже находил, но его исправление не дало ожидаемого результата, а других ошибок я не нашел. Проблема осложнялось тем, что блокчейн не предоставляет ни логгирования, ни отладки, поэтому написание кода сводилось к пристальному инспектированию кода глазами. Собственно, два дня отсюда и берутся — компиляция и прого тестов занимает минуты даже для простейших кейсов, а без отладки и/или логов трудно свести пример к MRE. В этом плане лишние буковки — это хорошо, а не плохо.
Ну, я не сторонник языков вроде J, поэтому для меня
currentIndex.hasValue
намного понятнее, чемcurrentIndex >= 0
. Ну и да, нужно понимать, что солидити это совершенно отвратительный язык без средств нормального выражения, в нормальном языке разница была бы гораздо меньше.S-e-n
На всякий случай спрошу, больше 1 раза newlyInsertedIndex нулевым быть не мог точно? Т.е. удалить, потом вставить опять — такого не было? И _не_ самым первым 0 индекс быть не мог тоже (хотя зачем в таком случае headindex)?
Потому что если такое могло быть — ошибка в обработке 0 индекса в самом начале. В коде с типами её нет.
PsyHaSTe
Нет, массив работал только на вставку.
S-e-n
Она всегда начиналась с 0 индекса (зачем тогда headindex вообще)? Т.е. условие в самом начале всегда выполнялось, и могло быть выполнено только один раз (первый)?
PsyHaSTe
Потому что head это не первый индекс, а индекс элемента с минимальным значением поля, по которому мы сортируем (в нашем случае, это поле
dateRegNumPair
). Если вы вставляем в такой последовательностито головой будет
1
.S-e-n
Тогда вот он и баг, так можно только если всегда 0 индекс вставлять первым:
if (newlyInsertedIndex == 0) {
nodes[0].prev = -1;
nodes[0].next = -1;
return;
}
Кстати, второй вариант вообще не позволяет вставить в список элемент с индексом 0, если я правильно понимаю, т.к. просто сразу выбрасывается return-ом.
PsyHaSTe
И что тут не так? 0 индекс и так обычно вставляется первым, это дефолтное поведение. Вставили нулевой индекс — получили голову, следующего и предыдущего элемента у головы нет, этот факт мы и записываем. В чем баг заключается?
S-e-n
В том, что если первым вставляется не 0 индекс, то код в нескольких местах обращается к элементам, которых нет, и записывает их в next и prev элементов, один из которых есть, а другого — тоже нет. Как бы уже не особенно понятно, что в таких условиях будет происходить, а если ещё и попытавится вставить 0 элемент, который просто добавляет ни на что не указывающую ноду (но на которую может что-то указывать), ещё менее понятно.
PsyHaSTe
Видимо я плохо объяснил.
Массив всегда растет от нуля. То есть индексы всегда 0, 1, 2 и так далее.
Дальше, у нас есть вставленный в нашу арену (массив) элемент. И мы должны понять как нам заапдейтить наш linkedList чтобы в нем порядок обхода элементов остался правильным.
Если мы вставили нулевой элемент, то он сам с собой уже в порядке и делать ничего не надо. Если мы вставили первый элемент, то мы должно либо из него сделать новый хвост, либо новую голову.
Ну и так далее.
В чем вы тут видите противоречие? Можно пример, который всё поломает?
S-e-n
Проблемы будут, если массив не всегда растёт от нуля (для меня было это не очевидно).
PsyHaSTe
Вот и я так и не придумал, в чем проблема.
Возможно, я когда-то сделаю MRE чтобы выяснить, где косяк. Но, если честно, немного лень. Задача решилась, и я просто решил рассказать как оно было. Можно решить задачу без типчиков? Конечно можно. Но с типами оказалось проще.
kryvichh
Вы смешиваете для индексов знаковые и незнаковые типы uint64 и int. Как они ведут себя при присваиваниях, при сравнениях в конкретной реализации языка? Delphi бы вам такого не простила, пришлось бы везде принудительно писать приведения типов (что есть плохо). Если для индекса достаточно диапазона 0..2147483647, и -1 для обозначения отсутствия ссылки, то лучше везде использовать int.
PsyHaSTe
Где я смешиваю? Везде где я смешиваю я пишу явный каст, например
uint64(index)
, причем только после проверки что число неотрицательное.Во-первых куча языков не позволит вам получить значения из массива по знаковому индексу (и емнип это верно для солидити). А во-вторых я предпочитаю хранить факт проверки на неотрицательность числа в типах, а не в голове. О чем, собственно, и статья.
kryvichh
Да, вот явный каст и есть плохо: получается, мы не используем типы в типизированном языке в полной мере. В Delphi можно написать например вот так:
И программа будет перепроверять значение индекса на выход за границы при каждом присвоении, при включении соотв. опции компиляции, как в Runtime (Range Check Error), так по возможности и в Compile time.
Ошибка у вас была как минимум здесь:
Вот реализация на Delphi (not tested):
Shatun
Есть еще преимущества например типизированной джавы vs джаваскрипт. Допутим у нас очень простой веб-сервис, мы на джаваскрипте анписали логику за час, на джаве-за полтора.
Но после создания веб-сервиса на спринге у меня сваггер будет сгенерирован автоматически из указанных мной типов, валидация входящих параметров также появится сама собой.
На джаваскрипте мне придется или добавлять те же типы и из них генерить сваггер или руками самому держать сваггер в акутальном состоянии. Валидация делается обычно также отдельно, например используется joi, для которого по сути я указываю те же типы еще раз, но в другом формате. И после этого мне нужны тесты чтобы быть уверенным что возвращаемые данные всегда соотвствуют выходному парметру-в джаве же в этом я буду уверен и так.
По факту у меня не выходит написание микросервсиов готовых к выходу на продакшен на джаваскрипте сильно быстрее. И это при том что в джаве все-таки не самая сильная система типов.
6opoDuJIo
Статической типизации избегают либо трусливые параноики («я не знаю что поменяется завтра, надо
сделать яп внутри япнафигачить всё динамически»), либо люди, которые не могут в формализацию.vanxant
… или просто не любят писать слишком много лишнего кода.
6opoDuJIo
на такой случай существует type deduction и implicit conversion
potan
Вывод типов позсоляет лишнего кода почти не писать, а полиморфизм писать кода даже меньше, чем в большенстве динамически типизированных языках (ну может кроме Julia и Common Lisp).
К тому же IDE часто позволяет генерировать код исходя из информации о типах.
vanxant
Полиморфизм никак не связан со строгостью системы типов. И да, для динамически типизированных языков тоже придумали IDE с автогенерацией кода.
0xd34df00d
Это скорее эмпирическое наблюдение: почему-то наличие параметрического полиморфизма оказывается скоррелированным со строгостью системы типов.
potan
Для полиморфизма по параметризованным типам (например монадам) требуются параметризованные типы, из динамических языков я такие знаю только в Julia (в CL можно сделать, но я не видел и сам не пробовал).
Современные IDE пытаются изобразить статически типизированный язык из динамически типизированного, прогоняя вывод типов. Но информации для полноценной реализации Type-Driven Development у них просто нет.
nlinker Автор
Вот, кстати, да.
Если IDE легко справляется с выводом типов в программе на динамически типизированном языке (правильно подсказывает допустимые операции, находит точки использования и не ошибается при переименовании), это просто означает, что в программе никакого динамизма и нет вовсе.
Можно было бы тогда взять просто статически типизированный язык, и тем самым воспользоваться уже имеющейся проверкой типов и преимуществами с Type-Driven Development.
AlexBin
Еще раз повторю (я уже устал, правда): просто взяв статический язык, вы полностью лишаете себя динамичности (об этом ниже), которая иногда очень выручает. Иногда нам нужно сначала быстро решить проблему, а уже потом решить хорошо, это реальность. Так вот динамичность позволяет творить невероятную магию.
В статической типизации вы либо лишаете себя этой магии, либо язык пытается ее каким-то образом реализовать, отвечая на запрос сообщества. А раз он пытается реализовать динамичность, то «можно было бы тогда взять просто динамический язык» (с)
Я не против статичности или динамичности, я против фанатизма.
Kanut
Я постоянно встречаю подобные заявления, но вот на практике в более-менее сложных продуктах я такого не видел ни разу.
Ну или если сформулировать иначе, то почему-то в итоге «динамические решения» оказывались гораздо затратнее…
У вас всегда есть что-то вроде базового «object», который вам позволит забить на статическую типизацию и вы можете делать вид что у вас типизация динамическая. Вот только зачем?
AlexBin
Конечно, ведь не существует в мире крутых качественных проектов, где под капотом динамика. Ровно как и не существует плохих глючных проектов, написанных на статике. Практика бывает разная, и каждый оценивает через призму своего опыта. У вас такой опыт и такая статистика. Я же просто призываю людей быть объективными и выключить фанатизм.
Мне второй раз написать, зачем?
Kanut
А этого я нигде и не утверждал. Но на мой взгляд такие варианты в итоге оказывались всё-таки затратнее.
Ну если вам удастся во второй раз хорошо аргументировать, то было бы неплохо.
Но вот про какую-то «невероятную магию» и вещи вроде «Иногда нам нужно сначала быстро решить проблему, а уже потом решить хорошо» писать пожалуй не стоит.
На мой взгляд какого-то особого выигрыша во времени «динамические варианты» не дают. Как минимум если учитывать имеющиеся на сегодняшний день тулсы и фреймворки и более-менее уметь ими пользоваться.
AlexBin
Хорошо, если в этот аргумент вы не верите, тогда мне нечего ответить.
В любом случае, я тоже могу ошибаться. Языков так много, и возможно мы просто сравниваем неудачные и удачные случаи на неудачных и удачных языках в неудачных или удачных проектах.
Но я убежден, что гибкие высокоуровневые языки в современном мире должны предоставлять возможность указывать тип и возможность творить магию, динамический ли это язык, или статический.
Kanut
Как Кларк написал в одном из своих законов: «Любая достаточно развитая технология неотличима от магии».
Что в общем-то означает что магия это технология, которая просто слишком развита для понимания тем, кто считает её магией. Так что как по мне то никакой магии нам в информатике не надо :)
0xd34df00d
Из существования крутых качественных проектов, где под капотом динамика, не следует ваш предыдущий тезис (что её нельзя было реализовать на статически типизированном языке).
0xd34df00d
Можно пример таких задач?
vanxant
Распарсить мегабайтный json или xml, поменять там пару строк и запарсить обратно. Сейчас у нас везде микросервисы в облаках, которые не должны знать друг о друге. А с точки зрения юзера это всё ещё один документ (файл). В котором вы знаете свой селектор (не путь).
Kanut
И почему это по вашему мнению в статическом виде невозможно или прям таки сильно затратнее? Ну то есть в чём конкретно проблема должна заключаться?
vanxant
Если нормально готовить динамические языки, ты узнаешь об этом в момент прогона тестов. Также как и в статических по большей части, т.к. если у нас объявлен тип аргумента int и мы туда передали 0, а потом внутри библиотечной функции на него делим, статическая типизация не особо поможет.
PsyHaSTe
Только тесты на "я правильно понял докуметацию" обычно не делают. Потому что когда я вижу флоу реальных разработчиков, он скорее "написать как написано в доке, пройтись в дебаггере степовером и проверить что всё работает ожидаемым образом".
Да, поэтому в нормальной библиотеке у вас будет тип аргумента
NonZero<int>
, и ноль передать туда вы не сможете.vanxant
В буквоедство интереснее играть вдвоём. Теперь представьте, что у вас три числовых параметра a, b и c, и для них должны выполняться неравенства треугольника. Но только если Луна не находится в третьем Доме.
Дальше, предположим даже, что ваш язык позволяет накладывать подобные ограничения на типы. Много ли кто будет способен это корректно сделать? Много ли кто будет этим реально заморачиваться?
Наконец, главной претензией к динамической типизации называют стоимость дальнейшей поддержки. Хорошо, предположим вы выпустили библиотеку с NonZero<int>, она пошла в массы и обросла пользователями. Дальше выяснилось, что по новым веяниям законодательства/науки/бизнес-требований этот int таки может быть 0, просто нужно использовать чуть другую формулу. Но все кругом уже привыкли, что у вас NonZero<int>, хранят в своих структурах NonZero<int> и в куче мест делят на этот int, зная, что он точно NonZero. Короче, ваше, как оказалось, ошибочное требование расползлось по сотням кодовых баз. В языке с динамической типизацией вы бы просто поменяли реализацию своей функции. В статике вам нужно выпускать следующую версию апи, ломать обратную совместимость и заставлять всех медленно и мучительно обновляться.
mayorovp
Почему одно и то же изменение в динамике ломает API, а в статике — нет?
vanxant
Потому что в статических языках загрузчик библиотек ищет функции по их сигнатурам. Сигнатура изменилась? Всё, не загружается, иди как минимум перекомпилируй.
Веселье начинается, когда библиотеку В уже обновили и она теперь требует новое АПИ, а библиотеку С ещё нет, а в вашей программе используются и В, и С одновременно.
В особо запущенных случаях так можно и до докера додуматься, да.
Kanut
Совсем не обязательно. Например вы можете создать DTO-класс и прописать его в сигнатуре. Изменение структуры самого DTO-класса на сигнатуру функции теперь уже никак не повлияет.
vanxant
Ну то есть вы предлагаете отменить статическую типизацию и заменить её динамической. Ясно-понятно.
Kanut
Нет, не предлагаю. Статическая типизация при этом никуда не пропадает.
MooNDeaR
Вроде вся статья была про ваше заблуждение, но вы так и не увидели проблемы?
Защититься от изменения формата данных можно на любой типизации. Изменение интерфейса (читай типа) — ни в каком.
Если вернуться к вашему примеру с делением на ноль, сотни тысяч строк кода на любом языке будут предполагать, что ноль передавать нельзя. И ничего страшного, если чего теперь передавать можно. Вы просто расширили интерфейс. Любой статически типизированный язык это легко переживёт. Все, кто передавал NonZero — так и будут это делать. Всем кому хочется 0 — вызовут другую функцию и всё.
А вот вам обратный пример. Теперь вы обязаны передавать ноль. По закону. Иначе тюрьма. В статической типизации я изменю сигнатуру кода и все сразу получат ошибку компиляции и не сядут в тюрьму. А вот чуваки с динамической типизацией должны будут исследовать сотни тысяч строк кода в надежде, что теперь никто не передаёт "0". И молиться что ничего не забыли. Иначе всё, тюрьма)
6opoDuJIo
Где же тут динамика?
PsyHaSTe
Делаете соответствующий тип. С завтипами и этого делать бы не пришлось, можно было бы просто написать что-то в духе
И без компиляторной проверки никакие "мамай клянус тот сервис отдает только правильные числа" вызвать бы не получилось. И это хорошо.
То есть молча поломать всех ваших клиентов — это благо? Не вижу разницы между этими случаями, кроме того, что в случае статики люди не смогут скомпилировать новую версию библиотеки, и им нужно будет обновить код (а пока они этим занимаются в проде успешно работает старая версия), а в случае хорошей динамики они узнают об этом когда ночью им позвонит L3 со словами "прод умер с ошибкой DivideByZeroException".
vanxant
Вы не поняли мой предыдущий комментарий. Строгая типизация накладывает ограничения не только на библиотеки, но и на пользователей этих библиотек, даже если им эти ограничения нафиг не нужны. А люди имеют привычку немножечко лениться. В нашем примере, вместо того, чтобы каждый раз при вызове функции превращать int в NonZero<int>, они проверяют один раз и просто хранят NonZero<int> — хотя им, допустим, всё равно, ноль там или нет. При обновлении апи проблемы возникнут именно из-за этого.
GrimMaple
В С++, например, если у NonZero есть конструктор, который принимает int, то никаких проблем не будет. Мне кажется, что эти «ограничения на юзеров» есть фича строгой типизации.
vanxant
Проблем будет.
1. Даже если есть тривиальный конструктор, его кто-то должен формально вызвать. Линковщик о нём ничего не знает, так что минимум перекомпиляция.
2. Может не сработать, если этот конструктор explicit, или если нарушается правило «не более одного неявного преобразования». Например, изначальный тип — short, в int он будет преобразован, а вот в NonZero<int> уже нет.
MooNDeaR
Напишите шаблонный конструктор (и заодно преобразование обратно), для всех типов соответствующих трейту std::is_integral?)
Вы явно недооцениваете современные статически-типизированные языки в их возможности обобщать :)
6opoDuJIo
Ага, а обратной совместимости никогда не существовало и не будет существовать.
0xd34df00d
В языке со статической — тоже, если тип аргумента нигде не указан, а выводится.
И да, давайте в обратную сторону. Требованием законодательства там теперь запрещён не только ноль, но и единица. Ваши динамически типизированные действия?
6opoDuJIo
Тесты защищают только тогда, когда они старательно пишутся и так-же старательно запускаются.
potan
Хорошие тесты писать сложнее, чем хорошие типы. Да и прогон тестов обычно долше компиляции.
0xd34df00d
То есть, вместо того, чтобы просто использовать уже готовую систему типов, которая индуктивно гарантирует корректность всей программы, вы будете выписывать частный сулчай этого индуктивного доказательства через тесты.
Удобно, ничего не скажешь.
t3hk0d3
Это только в волшебном мире с единорогами и радугами.
В реальном мире будет Segmentation Fault / Unexpected type exception.
0xd34df00d
Ну, буду знать, что хаскель — это волшебный мир с единорогами и радугами.
Серьёзно, ни разу не получал там сегфолты. И unexpected type exception не получал (более того, даже не знаю, что это такое).
MooNDeaR
Unexpected type exception — это вот то самое из мира с динамической типизацией. Запихнули гавно и узнали об этом на продакшене :)
AnthonyMikh
Это когда запускаешь код с
-fdefer-type-errors
0xd34df00d
А, так вот оно зачем нужно!
qellex
А можно уточнить, какое принципиальное отличие между спагетти-кодом на питоне и на го? Есть ли какой-то секретный закон, согласно которому 10 хороших программистов обязательно напишут плохой код на динамическом языке и 10 откровенно плохих программистов напишут идеально поддерживаемый код на статически типизированном?
AlexSpaizNet
Да дело даже не в хорошем или плохом коде. Со временем когда mvp или poc взлетел, код будет изменяться с огромной скорость. Через годик-два, если это не статический язык, добавление строчки кода будет опаснее коронавируса =)) И что бы каждый раз не какать в штаны при деплое, будут писаться тесты на самые элементарные вещи, но это не всегда будет помогать. Люди будут передавать инлайн обьекты, попутно добавляя и убирая фиелды… ты будешь смотреть на код и не понимать с какими структурами данных ты работаешь, потому что их нет. Люди будут делать волшебные вещи которые язык делать позволяет.
Рефактор? Это ад. Дай мне программу на го, удали часть когда, я его восстановлю и починю только глядя на ошибки компайлера.
Главный посыл — в шорт-терм, динамические языки рулят. Лонг-терм, поддержка и добавление изменений это сущий ад, и дальнейшее развитие продукта будет замедленно.
з.ы.
Вне веба, имхо немного другие критерии. Например обработку текста и матриц делать на го или жаве, то еще удовольствие… а вот на пайтоне — рай для души.
potan
Обработка матриц на пайтоне без внешних библиотек?
chersanya
Что за искусственное ограничение? Если считать использование внешних библиотек минусом, то тогда лучший язык — тот, который всё в stdlib впихнул?
AnthonyMikh
Просто тот же numpy по факту не на Python написан. И это добавляет головной боли с компиляцией нативного кода
chersanya
Во многих языках часто используется BLAS, который написан на фортране. Никакой проблемы в этом нет.
Но это всё-таки несколько другое дело, чем просто «написан на другом языке». Ни на каком языке не делают написание «с нуля», т.е. без обвязки и библиотек на других языках.
AnthonyMikh
Да ну?
chersanya
Возможно неточно выразился — имел в виду, что новые языки не обходятся без хоть каких-то библиотек, написанных на старых; не обязательно про матричные вычисления.
potan
Проблемы возникают при попытке запустить это в сколь-нибудь нестандартном окружении. То нужного компилятора нет, то .so/.dll не ищется, то случайно цепляется python из другого пакета.
chersanya
Так всё равно для хорошей производительности матричных и т.п. вычислений нужен blas, который идёт отдельной библиотекой. Причём часто даже без исходников — MKL широко используется. Ну а если blas быстрый не нужен, то numpy на x86 и arm по крайней мере легко устанавливается, никогда с ним не было проблем (с другими, намного менее популярными библиотеками в питоне, бывали).
potan
Во первых, управление библиотеками — не самое приятное занятие, когда собираешься быстро решить задачу. Тем более что работа с матрицами во многих языках уже сразу хорошо реализована.
Во вторых, идеоматические приемы работы с numpy заметно отличаются от ванильного питона. Да и вообще современные библиотеки (во всех языках) уже тянут на отдельный DSL и каждую изучать придется фактически как новый язык.
chersanya
Не могу согласиться совсем.
Задач, для которых используются языки программирования, огромная куча, и постоянно появляются новые. Без библиотек в любом случае никуда — нельзя всё нужное в стандартную поставку запихать.
В каких «многих»? Может быть в C, C++, C#, Java, Ruby, PHP удобная работа с матрицами? В питоне numpy является единственным используемым вариантом по сути, любая библиотека где нужны числовые массивы его поддерживает/использует.
AnthonyMikh
А что вы будете делать, если вы добавили в структуру новое поле и нужно отследить, что это поле правильно везде инициализируется?
chersanya
Интересное наблюдение. Я на go никогда не писал, но это утверждение по сути означает, что типичный код на нём очень сильно избыточный, раз его часть можно без особых потерь восстановить?
potan
На Elm писать однозначно быстрее, чем на жаваскрипте.
MooNDeaR
Меня лично удивляет, как описанное в статье можно не понимать) Ну, я имею ввиду если ты достаточно давно программируешь, все утверждения из статьи должны быть очевидными.
Я в С++ могу нафигачить хэш таблиц с void* указателями и кастить их к нужным типам только там где нужно. Просто это будет МЕДЛЕННО. Зато гибко. Вот собственно и вся история против динамической типизации и за статическую :)
0xd34df00d
Медленно в рантайме? Совсем не факт.
Отсутствие статической типизации не означает просаживание в скорости — вспомните о существовании ассемблера, в конце концов.
nlinker Автор
Но если добавить ещё одно обязательное требование — безопасность (в слабом смысле, то есть когда объекты или другие сущности изолированы друг от друга), то уже от проверок в рантайме уже не уйти, почти каждый доступ к объекту сопровождается накладными расходами, разве не так???
Конечно, есть исследования по gradual typing, когда система построенная на динамических типах постепенно обрастает статическими типами, и это позволяет постепенно избавляться от рантайм-проверок, но у этого подхода тоже есть существенные недостатки (см исследования учёного по имени Matthias Felleisen).
0xd34df00d
Безусловно. Более того, я вообще большой апологет статической типизации, просто хотелось подчеркнуть, что производительность не обязательно её требует.
Даже наоборот — выразил вот прям сейчас в соседнем окне кое-что через GADT для пущей безопасности и потерял процентов 30% производительности, так как компилятор больше не может чего-то заинлайнить. Приходится смотреть на GHC Core на ночь глядя.
MooNDeaR
Не совсем корректный пример с ассемблером. Там вообще нет типизации. Поэтому, строго говоря, для производительности вообще типизация не нужна, ни статическая, ни динамическая.
Вот только динамическая всегда, просто по определению, несёт следующие проблемы с производительностью:
1) Данные почти всегда неизвестного размера и неизвестной формы. Тяжело применить любые оптимизации.
2) Всегда нужно валидировать и валидировать много. Это постоянные if-ы, явные или не очень, при любом обращении к объекту. Сильно ломает уже оптимизации железные.
Этих двух пунктов достаточно, чтобы сделать код очень медленным :)
Вообще, пишите на С++ :) Там есть и статическая типизация и динамическая (шаблоны).
Temtaime
C++ мёртв, он обрастает костылями с невиданной скоростью, писать на нём в 2к20 отвратно.
Лучше уж Nim, D, Go, Rust.
MooNDeaR
C++ живее всех живых, к сожалению или к счастью решать уже не мне :) Я тоже топлю за Rust, но во-первых, этот язык позиционирует себя как более безопасная замена Си (не С++). Хотя конечно в целом, он имеет все шансы лет через 15 его заменить.
P.S.
На С++20, кстати, писать не так уж и отвратно. Плюсы отвратно учить, это да.
nlinker Автор
Я пожалуй ворвусь, и скажу, что всё-таки Rust нацеливается на то, чтобы сдвинуть C++. Это будет сделать трудно, из-за огромного количества легаси, но тут C++весьма неплохо сам себе помогает.
Совсем недавно встретился вот такой баг в современном C++ (который отчасти фича): https://wandbox.org/permlink/7sbsqzhbo0o7dOse
MooNDeaR
Не то, чтобы это был баг. Деструкторы для member-ов не вызываются в случае исключения в конструкторе еще с С++98. Именно поэтому все классы, принимающие лочки в себя, делают это всегда по ссылке.
Сказал бы я, что это "нормально", но нет, конечно это не так :)
develop7
«Это нормально. Но неправильно.»
netch80
> Деструкторы для member-ов не вызываются в случае исключения в конструкторе еще с С++98.
Вызываются, если соответствующие конструирования уже были завершены в конструкторе.
MooNDeaR
Да, моя ошибка, затупил.
6opoDuJIo
В примере по ссылке поведение зависит от компилятора. Если выбрать clang — работает как надо, деструктор вызывается.
potan
Без HTK с бустом тяжело конкурировать.
6opoDuJIo
Если выбрать clang, то работает как должно — деструктор срабатывает.
Вероятно, это баг в gcc
NooneAtAll
а можно подробнее об "отчасти фича"?
nlinker Автор
Я считаю это кумулятивным эффектом от следующих фич C++: исключения, в том числе и в конструкторе, неопределённого порядка передачи параметров при конструировании {}, правил вызова конструкторов/деструкторов во время исключений и плюс эффект какого-то очень нетривиального бага в gcc.
Во-всяком случае если этот код чуть-чуть подправлять по-разному, то баг исчезает. И вдобавок не воспроизводится в clang (но вероятно, там свои тоже очень нетривиальные).
getsiu
Это вроде бы понятная фича языка. Если конструктор объекта не завершился, а был прерван exception'ом, то деструктор не позовётся. Как можно сделать иначе? При этом все деструкторы для уже сконструированных объектов будут вызваны. В чём тут проблема?
nlinker Автор
Дело в том, что в примере выше конструктор вызывается, а деструктор — нет. То есть создан такой сценарий использования
lock_guard_ext
, при котором идиома RAII развалилась, и это кмк весьма неприятно.getsiu
Там кажется компилятор слишком вольно реализуют aggregate initialization, если явно задать конструктор, то деструктор у lock'а зовётся там где ожидается. В целом да, выглядит как баг в gcc. AFAIK, в gcc про это (evaluation order и т.д.) достаточно багов было.
0xd34df00d
Именно. И это утверждение совместно с тезисом «статическая типизация для производительности не обязательна».
Не нужно, если вы как-то что-то доказали снаружи.
Представьте себе, что вы взяли программу на идрисе, прогнали её через тайпчекер, убедились, что она работает, а потом стёрли все аннотации типов. У вас получилась программа без единого типа, но если вы её теперь скомпилируете и запустите, то проверять ничего лишнего вам не придётся.
Не, я чё-т устал. Как раз в черновиках уже ваяю статью-нытьё на тему.
Шаблоны — точно такая же статическая типизация, просто куда более близкая к структурной, чем к номинативной.
MooNDeaR
Я хотел сказать, что статическая типизация и производительность вещи зависимые, однако корреляция между ними определенно есть и явно неспроста.
Дело ведь не только во мне. Могу поставить деньги на то, что код сгенерированный идрисом при наличии информации о типах, будет эффективнее соптимизирован нежели код, без типов.
К тому же, это только "на бумаге", мы можем быть в чем-то уверены, т.к. мы взяли и проверили все типы заранее, а вот машина без этой информации тут же нагенерирует if-ов и косвенных обращений, потому что как только стёрли типы — компилятор их тут же забыл. А если не забыл, то значит вы не убрали типизацию :)
WASD1
Вообще подчёркнутое предложение в общем случае не верно. В смысле оверхэд на if, конечно остаётся, но «железные оптимизации» в нормальном языке это не ломает. Т.к. если вы пишите в языке с требованиями к производительности, то у вас должен быть какой-то аналог assert или unlikely / never макросов, — выравнивающий поток управления (т.е. штатный код всегда должен делаться по провалам, т.е. без джампов).
MooNDeaR
Ну, вот поздравляю, вы прикрутили типизацию :) Вместо явного описания типа, вы написали assert-ов :)
WASD1
хм… а я разве этот тезис оспаривал?
Оспаривал-то утверждение "(помимо неопределённого размера) if внутри runtime-типизации ломает аппаратные оптимизации".
Нет не ломает (есть средства чтобы не ломались) хД
yanchak01
Если так говорить, то каждому своё и можно оооооочень долго спорить что лучше или хуже. Я всё-таки оставлю свой голос за статикой, но нельзя отрицать, что динамические решения сейчас (как и в принципе всегда с момента их появления) активно продвигаются и во всю юзаются. Даже если взять C#, например, то даже и там используешь ключевое слово dynamic и вот тебе динамизм без всякого псевдодинамизма в виде приведения типов)))
KvanTTT
Для чего вы его используете? Я, наоборот, от него избавлялся в одном проекте — все же лучше все варианты прописать статически, да и работает dynamic медленней.
yanchak01
Был случай, когда приходило 8 различных вариантов, нужно было все 8 прописать?
lehkap
а почему нет? Если это действительно 8 разных, то и обрабатываться должны по разному, значит 8 разных обработчиков написаны, почему бы не написать 8 разных DTO.
yanchak01
А если их будет больше, и наперёд не знаешь какие они будут, то к примеру 10 раз запускать дебаг и каждый раз по одной создавать? При статике действительно будет быстрее, но иногда динамизм удобнее.
Cerberuser
А если наперёд неизвестно, какие они будут, то как их вообще обрабатывать? А если несколько категорий должны обрабатываться одинаково — то и тип у них в текущей модели будет один и тот же (возможно, с довеском какого-нибудь динамического типа вроде JsValue).
mayorovp
А как вы не продебажите обработчик не создавая его заранее?
GrimMaple
Generics? Templates?
PsyHaSTe
Динамики делали в бородатые годы для компаний которые кучу ресурсов вложили в легаси COM-совместимые библиотеки. Для обычной разработки они не то что не нужны, а скорее антипаттерн. Учитывая средства кодогенерации даже десятки вариантов лучше один раз сгенерировать за полчасика, положить в проекте, и не трогать больше.
В самом худшем случае если прям вот вообще никуда без динамики то ипользовать соответствующий тип. Например, у нас одна апишка отдает подмножество JSON-объекта, причем какое именно — зависит от параметров запроса. И там мы используем
JObject
в качестве возвращаемого значения. Почти что динамик, но уже намного лучше. Потому что у него хоть и динамическая структура, но хотя бы эксепшн метод биндинга в рантайме с ним не словишь никак. Впрочем, в статье этот пример как раз и описан.kaljan
ну, когда API отдает по одному URL разные типы объектов — этот не REST
PsyHaSTe
Она отдает один тип — хэшмапу ключ-значение.
Tanner
Я бы переформулировал это так: чтобы не писать на уже готовом динамическом языке, который фу-фу-фу, мы (в очередной раз) имплементируем его часть ? динамическую систему типов ? на нашем любимом статически типизируемом языке. Потому что
Serializable
? это же только интерфейс. В общем, NIH-синдром в полный рост.TheShock
Дженерик-функция не динамична. Nih тут вообще ни при чем.
Tanner
Тогда зачем стопицотый проект на C, C++ или Java в стопицотый раз имплементирует динамическую систему типов вместо того, чтобы использовать по назначению уже имеющиеся в наличии Lua или там Groovy? По мне так явный NIH-синдром.
bay73
Очень часто, из-за неумения готовить. Разработчики приходят из Javascript фронтенда в бэк на Java и пытаются там воспроизвести привычный мир.
Tanner
Вряд ли это основная причина. Скорее, наоборот, вполне себе опытные разработчики на статически типизированных ЯП не понимают, что такое динамические системы типов и зачем они нужны (некоторые вообще отрицают существование динамических типов). И поэтому вынуждены каждый раз переизобретать их.
bay73
Вы делаете ничем не обоснованное предположение о мотивах и, базируясь на этом предположении, объявляете разработчиков на статически типизированных языках, по сути, глупцами.
Не вижу в таком варианте дискуссии почвы для содержательного обсуждения.
Tanner
Когда вам говорят, что вы чего-то не знаете или не понимаете, а вы приравниваете это к обвинению в глупости ? это в вас говорит снобизм или чувство превосходства. Мы все можем не знать или не понимать чего-либо, независимо от уровня нашего интеллекта. Но, чтобы уметь учиться, нужно быть открытым к разным точкам зрения.
А если вы хотите обоснований, можете познакомиться с проектом, в котором я долгое время варился. Он имеет весьма навороченную систему динамической типизации, маскирующуюся под сериализатор данных. Эта система порождает множество проблем, начиная с мелких багов и кончая принципиальной сложностью её более-менее интероперабильной, кросплатформенной реализации. А между тем разработчикам достаточно было встроить какой-нибудь динамический язык (благо, на JVM реализовано много универсальных ЯП: Python, Ruby, JavaScript, Lisp, Tcl), чтобы забыть о реестрах типов, версионировании объектов (Duck Typing), интеропе (те же ЯП встраиваются и в C++, и в C#) и много о чём ещё. Заодно и облегчить подключение кастомного кода при распределённых вычислениях. А критичные части системы (сеть, обнаружение нод, алгоритмы дупликации и восстановления данных) оставить как есть.
И хуже всего то, что разработчики и архитекторы на этом проекте действительно сильные и опытные, без иронии. Но, видимо, уверены в превосходстве статической типизации, как и вы, поэтому других вариантов, кроме как пилить велосипед, не видят (или не увидели вовремя, а теперь уже поздно).
Иными словами, я не против статически типизированных языков. Но когда то, что вы пишете на статическом языке, начинает сильно напоминать интерпретатор динамического языка (или, как минимум, его часть), имеет смысл не переизобретать велосипед, а выбрать готовый.
mayorovp
И что, от выбора языка с динамической типизацией проблема сериализации данных решилась бы сама собой?
Tanner
Статья, перевод которой мы сейчас читаем, написана фактически как продолжение дискуссии о том, действительно ли парсинг JSON в статических языках является таким уж адом, каким его зачастую описывают, или ничего, можно терпеть.
Автор вот говорит, что можно терпеть, и даже описывает какие-то best practices. Но я чаще слышу противоположное мнение. А лучшие практики ИМХО ? это такие практики, которым проще следовать, чем не следовать. Когда их знаешь, конечно.
mayorovp
А причём тут JSON, если в Apache Ignite используется свой формат сообщений?
Tanner
Я пытаюсь сказать, что динамические данные проще парсить динамическим языком.
mayorovp
Дык они не динамические, там как раз статическая структура требуется. Даже если вы её красиво распарсите на динамическом языке — потом всё равно придётся её валидировать, с теми же самыми реестрами типов.
Tanner
Они сначала так и думали. Но в процессе оказалось, что именно динамическая, потому что ноды разнородные, и где какой тип данных объявлен, непонятно. И валидировать её централизованно не всегда возможно, так как обрабатывает её сторонний код.
TheShock
То есть фанаты динамичности накодили какой-то говняный рандомный непредсказумый протокол, с которым невозможно работать, а виноваты в этом те, кто предпочитают статику?
impwx
Tanner
Я именно этому доводу из статьи и оппонирую. Если динамические языки не помогают, то почему на статических языках так часто реализуют динамические системы типов?
impwx
Обратный процесс также существует — в Python и PHP добавили аннотации типов, для JS существуют Flow / Typescript. Но что это доказывает?
KvanTTT
Вероятно для того, чтобы поддерживать устаревшие технологии с наименьшей кровью?
0xd34df00d
Как часто?
Динамическую систему типов (неважно, что это оксюморон, ну да ладно) я реализовывал единожды, когда было интересно, что получится из статического типа
(ty : Type ** val : ty)
(не получилось, кстати, ничего, но это другой разговор). Встречался с чем-то таким я вообще ноль раз.Что было чуть чаще — стирание типов, когда полный статический тип значения мне не важен, а важно лишь то, что он реализует некоторый тайпкласс. Тогда да, тогда кому-то может показаться, что это начинает пахнуть динамической типизацией.
Самое близкое к динамической типизации, что я делал — это, но и там на самом деле статика с рантайм-проверкой тега, не более.
bay73
Tanner
Вы отчасти правы, у меня действительно «подгорает», когда мои любимые инструменты задвигают в угол. Но ответа на свой вопрос я всё-таки не получил. Допустим, го-свичеры написали cty, потому что заскучали по Пайтону, но как тогда насчёт GObject? А имплементациям этим нет числа, не зря появилось правило Гринспуна.
PsyHaSTe
Го по сути является языком, который сделан для людей которые пишут на динамических языках, и сам не особо статический, любую нетривиальную логику надо писать как
interface {}
черезinterface {}
, если конечно не хотите писать кучу кодгенов (которые еще запускать надо), а из средств выразительности только слайсы и хэшмапы.Не могу не привести одну подходящую под это цитату из самой обсуждаемой статьи:
Поэтому приводить в пример Go я бы не стал.
bay73
Ну вот я читаю описание cty и в первом абзаце вижу — «The primary intended use is for implementing configuration languages»
Аналогичная ситуация была в паре других проектов, где я варился — люди создавали отдельный язык для конфигурации приложения (то есть ядро системы было написана на одном языке, а для дополнительной конфигурации пользователем предлагался другой). Не знаю, как насчёт cty, но в двух мною наблюдаемых случаях это действительно давало возможность что-то быстренько «сконфигурировать», но помере роста требований к конфигурации и объёма, становилось неуправляемым. Разработчики ядра обычно в таких случаях делали вид, что их это не касается — «в ядре всё хорошо, а кастомизации это уже не наша забота».
foal
NIH — nerds in heaven?
mayorovp
Not Invented Here
foal
:) Мой вариант красивей, но именно ваш, скорей всего, автор комментария имел в виду.
ledocool
Мои полторы копейки.
Легаси. PHP. В одной из переменных класса есть член (тут можно ставить точку) AuthUser. Описания что это такое нет. Типа нет. Населена роботами.
Цель: вытащить информацию о пользователе из AuthUser, разобраться почему AuthUser иногда null и не возненавидеть макароны.
nlinker Автор
Да, об этом статья и говорит: у
AuthUser
, с которым вам приходится работать, есть тип, и у этого типа есть определение, но оно (определение) раскидано кусочками по всему коду, который использует этот объект тем или иным способом, то есть неявно.goldrobot
Я вот изредка натыкаюсь на истории про PHP подобные, и какой кайф что я его только пол годика назад начал применять. Нормальная типизация, уже даже в описании класса добавили возможость писать что типа имеет переменная. Я на нем пишу как на сишке, после питона как в раю.
ledocool
Откровенно говоря, не спасает. Статическая типизация не дает другим накидать мне непонятного дерьма, а то что у меня под капотом фурычит я и так в принципе знаю.
fori
Безусловно все описанное в статье очевидно и трудно с этим поспорить. Но где бы найти такой язык с полной инфраструктурой кроссплатформенной (иде, дебаггер, профайлер) и с хорошим сообществом, у которого была бы сильная типизация и возможность легко абстрагироваться от не нужной информации и легко вводить нужную. Мой профессиональный язык кажется далек от этого. Например для индекса в массиве заставляет указывать количество битов, и не позволяет зашить в нем ограничение на выход из диапазона указанного масива.
MooNDeaR
Rust? Разве что IDE нет, но VS Code справляется неплохо и есть плагин для CLion, но им не пользовался, не подскажу как работает. Тулинг в целом какой-никакой есть. Есть лайфтаймы и от них больно, но в сущности, как и ловушка с динамической типизацией — если не писать лайфтаймы, это не значит, что их нет :) Всё равно придется о них думать.
RealFLYNN
Я программирую на Rust, используя VSCode + rust-analyzer.
PsyHaSTe
Учитывая ваш текущий язык возможно вам подойдет Rust. Для обычных крудов подойдет Scala. Для эстетов — Haskell.
fori
В последнее время смотрю в сторону Haskell, оч нравится лаконичность и абстрагированность, но к сожалению с Windows он не особо дружен. Запустить GHCi и создавать простые проекты еще можно, но как только пытаешься использовать специфичные библиотеки или что то настроить под свое окружение, сразу натыкаешься на кучу проблем, связанных с тем что винда все же не его родная платформа.
Из функциональных еще рассматривал F# так как это dotNet, но кажется слабоват и недостаточно чистый по сравнению с Haskell.
PsyHaSTe
F# действительно слабоват, в основном это проблема сообщества, которому "нинужна" никаких фичей из более мощных систем типов. Знакомая риторика, нда.
Что до хаскелля на винде — у меня вполне успешно получилось написать прототипный проект на
servant + servant-swagger + persistent
. То есть по крайней мере ходить в постгрю и отдавать жсоны можно без проблем. Возможно глубже там какие-то проблемы, глубоко я не копал, но в первом приближении можно и так.Во-втором приближении можно собирать в wsl или докере, это довольно популярно.
Ну а со скалой никаких проблем нет. Там немного шумный синтаксис и местами странный сахар, в остальном проблем нет: весь жвм стек со своими либами доступен, на винде/линуксе работает отлично, мета-программирование в дотти обещает быть очень крутым.
В общем, варианты есть.
KvanTTT
Возможно также это и ограничение IL, который проектировался под ООП языки: C# и Visual Basic. По крайней мере ограничение по производительности.
Ну и пробовал немного F# — не зашел, не нашел достаточной причины, чтобы перелезать с C#. Либо не распробовал, либо мне слишком нравится писать производительный код, который не напишешь в F#, а если и напишешь, то выглядеть он будет отвратно.
PsyHaSTe
Нет, основная проблема не ограничения IL, а именно что сообщество. Ну смотрите сами, в чатиках люди иронизируют над нужностью хкт, сами разработчики языка их успешно поддерживают с этой точкой зрения, ну и так далее. Примеров больше, но мне немного лень их все находить. Можете попробовать пообщаться с коммьюнити, это достаточно быстро станет очевидно и вам.
september669
Kotlin?
PsyHaSTe
А что в нем принципиально хорошего? Это по сути джава с сахаром насыпанным жетбрейнсами, которые пока писали решарпер поняли, что им его не хватает. Нулляблы вместо опшнов, реифаид вместо честных хкт, адт нет (можно эмулировать через when is, но это такое).
Ну то есть я не спорю, язык неплохой, если ты его уже знаешь и на нем пишешь, примерно равен сишарпу с которого я сам никак не слезу. Но если не знаешь, то есть более вкусные варианты.
GrimMaple
D? Юзаю с VS Code и плагином code-d. Есть для него еще Visual D (плагин для Visual Studio) и какие-то другие IDE.
kryvichh
Если кроссплатформенности Win — OSX — Linux — iOS — Android достаточно, то… (одевает бронежилет и защитный шлем)… Delphi. В Web тоже можно, с thirdparty библиотеками.
fori
Кроссплатформенность — хорошо, но что там с зависимыми типами и другими ребрами лямбда куба? Есть большие сомнения. Но сам язык Pascal был в свое время не плох. Остались только хорошие воспоминания о нем со школы.
prostofilya
Elixir
Schrodinger_Kater
tl;dr. Так понял, речь идёт о сторонниках явного и неявного приведения типов. Лично для меня динамическая типизация проблемней тем, что требует дополнительных проверок типа перед проверкой значения. С другой стороны строгая типизация требует систематического явного преобразования типов, но по мне это небольшая жертва ради стабильности кода.
netricks
Номинативны ли классы в Пайтон? вот вопрос. Согласно определению номинативной структуры типов — да, номинативны, ибо два класса одной структуры, имея разное имя тождественными считаться не будут. Но при этом использование этих классов может быть в питоне абсолютно тождественным в силу динамической типизации.
Если мы рассматриваем открытость и стабильность системы типов, следует понимать структурность и номинативность, как структурность и номинативность интерфейсов, а не типов, поскольку открытость и стабильность системы типов завязана на интерфейсы объектов и способность к кооперации между собой.
Номинативность предполагает тождественность по имени, но в контексте интерфейсов, при динамической типизации нас интересует то, подходит ли объект по структуре, а не по имени.
Поэтому, либо классы пайтон надо считать структурными, либо что-то где-то еще не доверчено по терминологии номинальности и структурности.
В целом способность библиотек к кооперации между собой в динамических языках выше именно из-за того, что нет привязки к заранее сконструированным интерфейсам. Интерфейсы задаются неявно в момент использования в пользовательском коде, а не в момент конструирования класса автором библиотеки. То есть если в номинальном варианте интерфейс должен явно и жестко прописываться автором библиотеки, то при использовании структурной типизации интерфейс — штука довольно зыбкая. При структурной типизации автор пользовательского кода может использовать объект совсем не так, как предполагал автор библиотеки, что затруднено в номинальной системе. Или же автор пользовательского кода может наложить более слабые условия, чем улучшить взаимозаменяемость компонент. Этими вещами объясняется открытость.
Собственно, резюмируя мысль, открытость обеспечивается не структурностью типов, а структурностью интерфейсов типов. Языки, которые допускают структурное взаимодействие в интерфейсах показывают более хорошие результаты по интеграции библиотек. (В качестве примера, кстати, можно посмотреть на шаблоны в С++. Хотя система типов в С++ номинальна, шаблонные интерфейсы вполне себе структурны и имеют описанные свойства.)
P.S. Спасибо за статью. Отделение номинативной и структурной типизации от статической и динамической типизации многое ставит на места.
nlinker Автор
Да, вполне номинативны.
Другое дело, что можно экземпляры классов преобразовать к простым словарям и тем самым легко перейти к структурным типам. Но сами по себе классы
A
иB
различны.Да, именно так. В Python последствия того, что классы номинативны практически не заметны, поскольку, как вы написали, в подавляющем большинстве случаев от объектов нам нужны методы и поля, а не имена классов.
nlinker Автор
Да, вполне номинативны.
Другое дело, что можно экземпляры классов преобразовать к простым словарям и тем самым легко перейти к структурным типам. Но сами по себе классы
A
иB
различны.Да, именно так. В Python последствия того, что классы номинативны практически не заметны, поскольку, как вы написали, в подавляющем большинстве случаев от объектов нам нужны методы и поля, а не имена классов.
Но подождите, как пользователь сможет пользоваться неизвестным значением? Интерфейс всё же также задаётся автором библиотеки, но неявно, в виде какой-то функциональности, которую автор реализует.
Например, пользователь написал
x.foo()
. Значит ли это, что в библиотечном интерфейсе появился методfoo
? Нет. Этот метод там может быть (возможно в виде недокументированного кода или бэкдора), и уже является частью интерфейся, либо его там может не быть вообще. В обоих случаях интерфейс сформирован автором библиотеки.Единственная разница лишь в том, что в следующей версии библиотеки автор добавляя метод
foo
неявно магическим образом меняет определение в случае динамических языков, а в случае статических он выставляет новый номинативный тип (и это вызывает зубную боль, да,MyInterface2
), или мог бы выставить обновлённую версию структурного типа.netricks
Разница есть в силу того, что автор, конструируя номинативный интерфейс не всегда делает это оптимальным способом.
Допустим, в библиотеке есть некая функция Library.foo, которая принимает объект Library.IMyInterface.
Автор Library.IMyInterface мог потребовать реализации кучи разных методов, которые на самом деле в функции foo никогда не вызываются. Явное определение интерфейса практически всегда накладывает более строгие условия на получаемый функцией объект чем это необходимо. При динамической/структурной типизации требуется реализовать только то, что реально используется. Формально можно сказать, что для каждого метода, библиотеки, предназначенных для работы с однотипными объектами, требуемые интерфейсы этих объектов будут различны и всегда минимальны в противоположность явному описанию интерфейса.
mayorovp
Уточнение: при динамической и структурной типизации.
PsyHaSTe
По-моему опыту наоборот. Ты не понимаешь, какие требования есть у библиотеки, поэтому отдаешь годобжект который всё умеет и у которого есть все возможные свойства в надежде что это будет достаточно. Недавно вон pdfkit использовал на ноде, пример из доки работает, а попытка разбить на функции код и передавать объекты кусочками — проваливается.
В итоге самый простой способ просто передавать PDFDocument объект в каждый метод, потому что он умеет всё.
Собственно, я бы не отказался от примера любой популярной библиотеки, которая требует больше чем ей нужно. Обычно (особенно при активном использовании генериков) функция выставляет минимально рабочий интерфейс
foo<IHasFoo + IHasBar + IHasBaz>()
и дальше вы с этим делаете что хотите.netricks
В качестве примера можно привести многие библиотеки обработки данных и целые фреймворки, завязанные на свои внутренние типы. Возьмём хотя бы Qt. Методы Qt принимают QString, QList, QHashTable, хотя по хорошему, они должны принимать не эти типы, а все похожие на… В результате, при интеграции Qt с прочими библиотеками в точках сопряжения появляется довольно много лишних операций преобразования типов.
Так же ведут себя матбиблиотеки, строящие операции над внутренними типами данных, в результате чего использовать в рамках одной задачи несколько разных матбиблиотек становится довольно накладным из-за постоянной смены оболочек. Довольно часто матбиблиотеки предоставляют средства интеграции, но это всё-таки костыль прикрученный сбоку.
То есть, есть проблема в том, что нельзя передать пользовательский тип в библиотечную функцию малой кровью. Нужно или конвертироваться, или наследоваться. В противоположность этому, тот же numpy способен выполнять многие операции над пользовательскими коллекциями без дополнительной обработки.
IHasFoo + IHasBar + IHasBaz — это очень хорошо. Это явное минимальное описание интерфейса. То есть автор подумал об интеграции и даже задокументировал своё решение в код. Проблема PDFDocument в том что автор не подумал об интеграции. И в номинальном и в структурном исполнении можно найти хорошие и плохие примеры. Я же, впрочем, утверждаю, что сделать хорошую интеграцию на явных интерфейсах сложнее чем на неявных. Это требует меньше телодвижений. Но и в том и в другом случае это требует проектирования, конечно.
PsyHaSTe
Ну это как раз-таки пример не очень хоршего проектирования. В типичной джаве там были бы IQString, IQList и так далее. В более интересных языках был бы тайпкласс QString/QList/..., которые можно было бы реализовывать для чужих библиотек.
В достаточно мощной системе типов особых телодвижений не требует. Например, в хаскелле есть такая штука как Generics (и это не те генерики которые обычно имеют в виду под этим словом), которая позволяет бесплатно преобразовывать похожие структуры:
Если интересно — репл.
Соответственно чтобы использовать любые структурно похожие типы клиенту достаточно написать
instance Convertible SomeThirdPartyType MyType
.Как по мне, это очень небольшая плата на все плюсы, что дает проверка статического анализатора.
netricks
Из этого следует сделать вывод, что создатели языков и библиотек со статическими и номинальными системами типов осознают проблему и стремятся дать средства к её решению. Это великолепно. Однако мы сейчас беседуем об глобальных свойствах вариантов типизаций. Сам факт того, что такие решения, как те, что приведены выше существуют, показывает наличие проблемы. С противоположной же стороны такой проблемы изначально нет.
Насколько это критично в инженерной практике — вопрос ситуативный.
Успех питона, баша, перла, а также текстовых форматов хранения данных (см. Искусство программирования для Unix) в качестве компонентов системной интеграции, показывает, что как минимум в некоторых задачах динамика превосходит статику.
P.S. Я позволю себе процитировать одного хорошего человека: «В инженерном деле нет ничего хуже, чем религия». Так будем же как и всегда помнить об особенностях инструментов и сообразно их применять.
PsyHaSTe
Весь вопрос в зоне церемонности, про которую я недавно статью переводил. Мейнстрим языке создают ложное ощущение, что типчики это дофига сложно и многословно. Но нет, это не так.
Что до питона и перла, то тут вопрос в том, когда появились эти самые трансцеремонные языки. И если взглянуть, оказывается, что не так давно — лет 10-15 назад. И если для конкретных фреймворков это огромный срок, то для индустрии в целом это совсем не так.
Согласен, писать CI скрипты на хаскеллях я бы не стал. Однако стоит задуматься о проекте хотя бы на 500+ строк, и тут уже возникает желание описать контракты, чтобы потом не сидеть с дебаггером и не писать тесты на очевидные вещи.
0xd34df00d
Я бы сказал, что эта проблема — очень частный случай того, зачем нужны тайпклассы.
0xd34df00d
Это недостаток плюсов вкупе с некоторой идеологией кутей, которые проектировались четверть века назад, когда с темплейтами всё было плохо, с оптимизирующими компиляторами всё было плохо, да даже с
std::string
всё было плохо.В кутях примерно 2017-го года разработки там бы было написано
std::string_view
,template<typename It> auto foobar(It begin, It end)
, и так далее. Собственно, подвижки в эту сторону есть.В кутях на каком-нибудь хаскеле было бы
и использование этих тайпклассов вместе со стандартными
Foldable
/Traversable
/IsList
.При этом сохраняется полная номинативность.
Именно. Просто явные интерфейсы требуют проектирования сразу, а не когда-то потом.
nlinker Автор
-
sasha_semen
Для каждого случая есть свой лучший способ реализации.
«Программисты делятся на 10 два типа — духовные последователи Платона и духовные последователи Гераклита.
Платонисты верят в идеальные формы, любят, когда компьютер делает именно то, что ему говорят, и готовы пойти ради этого на любые средства. Всякая неопределённость в поведении должна быть устранена, все побочные эффекты учтены, все входы и выходы записаны — иначе для чего нужны компьютеры, как не для того, чтобы железной логикой быть источником порядка среди непредсказуемых людей? Платонисты придумали статическую типизацию, конечные автоматы, таблицы переходов, Агду, формальную верификацию и соответствие Карри-Ховарда. Когда они подходят к задаче, их мечта — найти именно такую структуру, в которую эта задача идеально влезает. Идеально! Платонистов чаще всего можно встретить в embedded, разработке компиляторов, проектировании сверхнадёжных систем (авионики, например), hard real-time, микроядрах — в общем, чем хардкорнее, тем лучше.
Не таковы гераклитяне. Они-то знают, что совершенства не существует, что миром правит хаос, и нет никакого способа привнести порядок туда, где его не было и не может быть никогда. Неожиданности всегда появляются, системы всегда ломаются, учесть всё невозможно, и единственный способ выживать в таком мире — быть достаточно гибким и изворотливым, чтобы восстанавливать всё утраченное. Гераклитяне придумали позднее связывание, аннотации, юнит-тесты, прототипы, горячую замену кода, нулевые указатели, message passing, акторы и супервайзеры. Ну и Perl, само собой. Их мечта — чтобы всё хоть как-то работало, какой бы хаос ни происходил вокруг и какими бы безумными ни были начальные условия и входные данные — а следовательно, гераклитян часто можно встретить в big data, финансах, вебе, телекоммуникациях, devops и других местах, где правит бал Его Величество Случай.»
netricks
А Пифагорейцы изобрели функциональную парадигму :).
sasha_semen
А какая сволочь ООП изобрела?
netricks
Сложно сказать, но принципы ООП применялись еще во время древних царств задолго до того же Пифагора. Думаю, пальму первенства формализации ООП можно отдать Демокриту с его идеями как прообразами вещей. Ну, а дальше эта концепция проходит через руки того же Платона и попадает к христианским философам. В целом ООП — очень древняя концепция, происходящая из особенностей работы мозга, а именно необходимости к обобщению, поэтому, я бы сказал, что ООП было всегда.
sasha_semen
Самая веселуха — я три года писал на плюсах (билдер), прекрасно понимаю принципы и когда на новой работе в конце стажерки сказали прочитать лекцию по ООП — я реально вспотел пока въехал в статью на интуите, моя лекция получилась глубоко теоретическая и мозгодробительная, а в последующей лекции, которую читал уже руководитель я осилил только теорию, практику так в общем ни не начал.
После этого основы ОО проектирования я даже боюсь открывать.
Kanut
Хм, а что вы имеете ввиду под «основы ООП»? Все основы на википедии влезают на полстранички: wiki
sasha_semen
Читать лекции по статьям в вики так себе затея ))
www.intuit.ru/studies/courses/71/71/info Тут чуть больше) 17 лекций, по крайней мере я сам понял зачем применять ООП.
Kanut
Ну зачем нужно ООП на мой взгляд великолепно обьяснено в соседнем комментарии. А лекция… Я бы взял основы с вики и показал для них парочку небольших примеров.
А лекция с сслыки на этот самый интуит на мой взгляд написана совсем не для новичков и уже не об основах, а скорее о деталях :)
sasha_semen
Ну скажем я читал лекции стажерам, то есть выпускникам матфака. Объем вики — и даже возможно интуитовский курс по Бертрану Мейеру студентам скорее всего давали в той или иной мере. Да и вообще у меня есть небольшой опыт преподавания, в том числе и на базе интуитовских лекций. Когда название предмета дают за два дня до первой лекции очень знаете ли удобно. Да и вообще я не любитель готовится к лекциям))
EvgeniiR
…
То есть кто такой Алан Кей вы не знаете, а как лекции по ООП читать — пожалуйста.
PsyHaSTe
Только Кей изобрел не ООП, а акторную модель. То что он назвал свое изобретение "ООП" и в джаве штука так же называется всего лишь попытка всех запутать.
EvgeniiR
Кей изобрёл то что изобрёл(youtube, интервью), и назвал это ООП, а было до появления Java, сильно до. Actor Model продолжение тех идей «ООП».
Путаница пошла когда все подряд, в т.ч. Java, C++ и пр. начали использовать модное слово чтобы привлекать больше аудитории.
То что самому Кею название ООП не нравится — да. Вот его «извинение» от 97г. — youtu.be/oKg1hTOQXoY?t=2270, но историю не переписать.
А в данном случае проблема то не в определении, проблема в том что из-за путаницы термином ООП называют всё подряд, и лекции «по ООП» вне исторического контекста смысла не имеют, а когда сами лекторы этого не понимают начинается всякая чушь вроде примеров ООП в виде наследования Moderator от User и пр.
PsyHaSTe
Поэтому предложение использовать общепринятое определение, а не то, которое было изначально. А то можно дойти до того, чтобы называть калькуляторами людей, занимающихся бухгалтерией.
EvgeniiR
И какое такое общепринятое определение?
3 Кита — вздор, и с ними полно людей несогласно, да и в чём практическая ценность — не ясно.
PsyHaSTe
Ну я обычно использую определение "ООП это как в Java". Пусть не особо формальное, но зато ограничивает пространство возможностей, и ООП "по-кею" в него уже не попадает.
EvgeniiR
А что с практической ценностью?
Чего должно дать это определение кому-то? Зачем оно студентам?
Почему не дать просто принципы проектирования?
PsyHaSTe
А определения принципам не противоречат.
EvgeniiR
Зато головы студентам знатно засоряют, и рождают множество недопониманий приводящих к догматизму.
sasha_semen
Проектирования? Для этого есть специально обученные архитекторы.
sasha_semen
Я стажерам на работе лекции читал — собственно из всего коллектива педстаж только у меня был. И да — высшему образованию уже три тысячи лет и преподавателям в вузе профессиональные компетенции разработчика собственно особо не нужны — нужны педагогические, научные и административные компетенции. Сомневаюсь что профессионал будет читать лекции по ООП за 100 рублей в час, да и вообще меркантильные уже давно в бизнесе, в образовании в массе идейные.
chersanya
Часто преподают программирование те, кто собственно профессионально работает программистом, или кем-то кто программирование широко применяет в работе.
sasha_semen
Вот только не у всех профессионально работающих программистов есть хоть какие-то педагогические навыки. Ну и кто широко применяет программирование — архитектор БД — он вообще будет тратить своё время на преподавание?
chersanya
Вообще хоть какие-то педагогические навыки свойственны человеку в принципе, эволюционно. А заинтересованным студентам, как мне кажется, бывает важнее отлично знающий предмет преподаватель, чем умеющий более хорошо именно преподавать.
Все, или большинство — не будут. Но как показывает практика, такие люди находятся. Я сам понемногу преподаю (не программирование правда) просто потому, что нравится процесс; знаю несколько однокурсников, кто так же делает.
В дополнение к интересу у некоторых может быть и план научить + заинтересовать студентов, чтобы они шли в вашу команду/фирму.
sasha_semen
Студент очень часто заинтересовал лишь халявой, ибо с первого курса работает и хает систему высшего образования что там не дают «современных знаний». А по поводу педстажа — поверь, умение «на пальцах» доносить сложные вещи очень важно. У нас к примеру завкаф лекции читал — так там формула на формуле, до меня лет через 5 неравенство Белла только доехало. Зато в науке у него все норм было.
chersanya
Слишком сильное обобшение про студентов. Понятно, что люди разные бывают — но адекватных студентов хватает для того, чтобы было собственно интересно преподавать.
sasha_semen
Конечно, собственно для них и преподаешь. Это в школе всех надо учить, со студентами проще. Но от зачета на халяву ни один российский студент не откажется.
chersanya
Прям такого эксперимента мы не проводим :) Но регулярно заметное количество студентов переводится от преподов, про которые известно что они халявные, к другим (в обратную сторону, конечно, тоже часть переводятся). Ещё по некоторым предметам есть два уровня — условно «базовый» и «продвинутый», и в базовом меньше тем/задач; часть студентов выбирает продвинутый, безо всякого принуждения. Нигде в оценке не пишется, какой из уровней сдан.
leon_nikitin
Кстати, именно книга Бертрана Мейера меня вывела из болота ООП на свет ФП.
0xd34df00d
Последние главы из Пирса, конечно же!
Ну там, где ООП на лямбда-исчислении с сабтайпингом, bounded параметрическим полиморфизмом и конструкторами типов.
netricks
Иногда за обилием теоретических выкладок не видно тех идей, которые лежат в основе того или иного метода.
А идеи как правило очень просты. Однако, поскольку они просты, написано о них немного, а 99 процентов того, что написано, посвящено следствиям, а не причинам. А между тем изучать следствия без понимания причин довольно бесполезно.
Всё ООП состоит в одной единственной идее, или точнее в одной единственной проблеме.
Нам сложно осознать программу целиком. Программа необозрима и разнородна. И тогда мы принимаем естественное решение. Мы начинаем программу дробить на части и работать с частями независимо.
Давайте попробуем декомпозировать всё, что возможно. Давайте будем разбивать задачи на независимые подзадачи, давайте уменьшать количество информации которую необходимо понимать для того, чтобы разобраться в подзадаче. Давайте уменьшать количество связей в системе.
Всё направлено на то, чтобы мозг мог осознать то, что видят в отдельный момент времени глаза программиста.
Отсюда прямо следует идея инкапсуляции объектов, идея интерфейсов, идея переиспользования кода в том числе в виде наследования и все прочие идеи ООП.
Вся мозгодробильная теория — это не о том, что мы делаем, это о том, как мы делаем, а именно как мы реализуем это самое разбиение.
Логично спросить. А почему мы ставим во главу угла декомпозицию данных, а не алгоритмов. Ведь объекты это в первую очередь данные и связанные с ними алгоритмы, а не наоборот (необходимость упаковки данных вместе с методами прямо следует из идеи инкапсуляции)… И опять же, это следствие того, что нам проще работать с данными имеющими действия, чем с действиями имеющими данными.
Люди мыслят объектно и не умеют обмозговывать большое количество объектов в единицу времени. Вот в этом то собственно и суть ООП.
sasha_semen
Ага понятно. Но зачем? Что там мешает пользоваться функиональным программированием?
Модульность, повторное использование и прочие рассуждения о методе подходят и классическому процессу разработки. Но преимущество ООП возникает когда у нас в процессе разработки начинают изменяться изначальные требования.
Kanut
Чем сложнее у вас структуры и взаимодействия между ниму, тем проще в них запутаться. И на мой взгляд с определённых масштабов функциональное программирование начинает уступать ООП.
Как я уже написал выше дело скорее не в изменениях, а в том насколько громоздки и сложны имеющиеся структуры.
sasha_semen
Структуры вполне в С используются. И опять же — когда мы замораживаем требования на весь процесс разработки — можно обойтись и без ООП. Но да, с определенного размера ПО заморозить требования = ПО будет устаревшее на момент выхода.
Kanut
Ну под «структурами» я в данном случае имел ввиду нe «struct» из ЯП, а те «части реального мира», которые нужно моделировать.
Даже если забыть о том что «замороженные навечно требования» я ещё никогда в своей профессиональной карьере не встречал, то всё-таки надо понимать что проблема не только в этом. Если вам дадут ТЗ, в котором описание структур и их взаимодействия между собой будет по обьёму больше чем «Война и мир», то функциональные ЯП в этом случае будут не особо «оптимальны».
Но может кто-то видит это и по другому…
PsyHaSTe
Вы когда вызываете функцию, скажем,
foo
, вы ожидаете какого-то результата? Или вы просто вызываете случайные функции у случайных объектов и надеетесь, что результат пройдет QA?Kanut
Я не уверен что ваш комментарий пришёл по адресу. Я то как раз ничего против ООП и строгой типизации не имею :)
PsyHaSTe
Я не понимаю, при чем тут "функциональное программирование начнет уступать" просто. У вас в ФП точно так же есть объекты, методы, есть инкапсуляция поведения за функциями и тайпклассами.
От чего оно вдруг должно развалиться?
Kanut
Оно там может быть, но не обязано быть по определению. Точно так же отдельные элементы функционального программирования вполне себе могут встречаться в отдельных ОО-языках.
PsyHaSTe
Я вроде статью писал, идея ФП в ссылочной прозрачности. Поэтому да, не обязательно оно там будет, но как-то получается что на практике все фп языки которыми кто-то пользуются (scala/haskell/...) их имеет.
Kanut
Я знаю людей, которые вовсю пользуются Scheme. И там с ООП по моей памяти не то чтобы всё особо розово… :)
sasha_semen
Ха))) Я встречал и не один раз. И не два. Всякие там лабы и прочие курсовые, небольшие утилиты. Опять же матмоделирование и прочая «наука». В общем тоже имеет место быть. А когда ООП ради ООП начинают лепить, а особенно гугль-программисты… У нас тут прилично легаси-кода, на котором коллеги учились ООП программировать. Собственно примеры как не надо перед глазами))
Kanut
Большинство людей просто при прочих равных берут тот ЯП, который им более знаком и удобен. И по хорошему 90% всех задач одинаково хорошо/плохо решаются различными ЯП. то естъ это просто скорее дело вкуса.
sasha_semen
Хз. Мой диплом быстрее работал на фортране, специально проверял (чем на с++). Но параллелил код я конечно на сях. Опять же для бизнес задач — кобола (abap) вполне достаточно, но не думаю что кто нибудь начнет в здравом уме писать на нем драйверы. А на плюсах реализовывать свою веб страничку — кмк к пенсии только превед медвед на 80 порту наваяешь))
Kanut
Ну совсем до абсурда дело доводить конечно тоже не надо. Под «разными ЯП» я всё-таки имел ввиду не абсолютно всё множество имеющихся ЯП, а те из них которые приведут к удобоваримому результату за приемлимый отрезок времени :)
sasha_semen
Давай тогда остановимся что для каждого класса задач есть свои наиболее оптимальные языки. Грубо говоря — питон пхп джиэс для веба, с/asm для драйверов, с++ для разработки ОС и всякие там лиспы для имакса. И уважающий себя программист должен знать несколько языков желательно для нескольких сфер. Тогда выбор будет более оптимальным.
0xd34df00d
Почему жс для веба, понятно (по крайней мере, в мире без wasm). А почему какой-нибудь питон или пхп? Люди веб-сервисы и на джаве пишут, а я так вообще норм на хаскеле что-то ковыряю (и куда продуктивнее, чем на питоне, на нём соответствующие вещи я бы не осилил вообще никогда).
0xd34df00d
Таки нет, с CGI это делается за вечер, а с Wt оно за вечер делается так, что всё свистит, пердит и переливается.
netricks
А функциональное программирование не противоречит ООП, как и ООП не противоречит функциональному программированию. Эти концепции ортогональны. Вы можете писать код, руководствуясь принципами ООП и функциональными принципами одновременно. Следование ООП даст вам хорошую декомпозицию, а следование функциональной парадигме математическую строгость и надёжность, повторяемость, простоту тестирования.
Так же как и философские концепции не противоречат друг другу, парадигмы программирования тоже не соперничают. Они зачастую эксплуатируют общие идеи. Было бы странно, если бы ООП не эксплуатировало модульность, придуманную задолго до возникновения программирования, но ООП берет эту модульность и ставит в центр своей методологии.
Парадигмы программирования можно рассмотреть как некое подмножество приёмов программирования в комбинации продуцирующие код с некоторыми свойствами.
Приёмов очень много, а мы хотим получать более менее предсказуемые результаты. Поэтому мы используем парадигмы, зная что следование парадигме наделит результат проектирования — программу предсказуемыми свойствами.
sasha_semen
Возвращаясь к первому коменту в ветке — входные данные у нас не всегда предсказуемые, поэтому
в команде разработчиков нужны и платонисты и духовные последователи гераклитяне.имеет место быть и статическая и динамическая типизация))Kanut
Я не совсем понимаю что значит «непредсказуемые данные». У вас есть какие-то данные, которые сами содержат динамический код, который их должен обрабатывать?
Или у вас всё-таки обработка проходит в «вашем» коде?
sasha_semen
" Их мечта — чтобы всё хоть как-то работало, какой бы хаос ни происходил вокруг и какими бы безумными ни были начальные условия и входные данные — а следовательно, гераклитян часто можно встретить в big data, финансах, вебе, телекоммуникациях, devops и других местах, где правит бал Его Величество Случай."
Kanut
А это то здесь причём? Если у вас где-то написан код, который каким-то особым образом обрабaтывает какие-то «непредсказуемые данные», то эти данные на мой взгляд уже нельзя считать непредсказуемыми. И следовательно можно типизировать.
sasha_semen
Я тебе как физик скажу, что вселенной правит хаос. То есть результат работы кода уже непредсказуем (точнее предсказуем, но с определенной вероятностью). Или например квантовые компьютеры — там с кубитами и забитами вообще веселуха.
Kanut
Это не значит что хаос должен править в ЯП.
Это уже пошла философия, которая очень далеко выходит за рамки обсуждаемой темы :)
sasha_semen
Почему нет? Как программистов можно условно разделить на платонистов и гераклитян, так и яп можно разделить на и «статические» и «динамически» ориентированные)) Я кстати больше приверженец платона, но уже давно не перфекционист.
Kanut
Делить вы ЯП можете как вам угодно. Ваше личное дело. Но эти ваши «непредсказуемые данные» будут одинаково «непредсказуемы» и там и там. И об этом вобщем-то и речь в статье…
PsyHaSTe
К слову, цитируя Пирса
Yuuri
Таки противоречит. В ООП объекты существуют ради инкапсулированного состояния, которое может изменяться при вызове методов. ФП, ради ссылочной прозрачности, изменяемого состояния старается избегать.
netricks
Это хорошее замечание. Фокус здесь в том, что инкапсулированность состояния не требует его мутабельности.
Я привожу пример с функциональным выводом геометрических тел в результате выполнения булевых операций над примитивными телами.
Объект геометрического тела в brep представлении является сложным объектом в котором есть несколько уровней заинкапсулированных абстракций, но если мы используем эти объекты иммутабельно, мы можем использовать принципы функционального программирования для построения деревьев вычислений и строгого вывода сложных тел.
Таким образом, ФП может работать вместе с ООП при условии иммутабельности объектов.
Upd: правильнее сказать внешней иммутабельности. То есть объекты должны быть иммутабельно с точки зрения методов, следующих принципам ФП.
nlinker Автор
В таком виде методы становятся просто чистыми функциями, а состояние классов передаётся и возвращается через композицию этих функций.
Вопрос: зачем тогда нужны классы, кроме как формировать пространство имён для этих функций?
Без мутабельного состояния вообще термины "класс" и "метод" теряют смысл, нет?
netricks
Мне немного непонятно, почему класс и метод меняют свои смыслы, но да шут с ними...
Я покажу непротиворечивость методом временного разделения. В примере выше мы использовали объекты как аргументы чистых функций, чтобы получить функциональный вывод. Для этого мы потребовали иммутабельности объектов во время вывода. Но как только объект выведен, мы можем отбросить функциональную парадигму и использовать этот объект в полной мере задействуя приёмы ООП. Иммутабельности более не требуется.
nlinker Автор
Я согласен, что ООП и ФП не противоречат друг другу.
Правда, вы привели довольно нетипичный способ их скрещивания: когда мы создаём мутабельную структуру данных с помощью чистых функций, и потом во всей программе её используем. Смысла в этом не очень много: за корректностью этой структуры данных надо тщательно следить, аккуратно шарить между скоупами и потоками и не лезть в неё напрямую.
Правильнее будет наоборот, с помощью мутабельных функций построить начальное состояние иммутабельной структуры, а потом спокойно использовать её, пользуясь преимуществами ссылочной прозрачности чистых функций, без проблем разделять структуру между скоупами и потоками и тому подобное. (Этот подход даже в Java используется, см
StringBuilder
иString
).netricks
Я специально (возможно несколько неаккуратно) перевернул эту конструкцию, потому что иначе кто-то мог бы сказать, что с точки зрения ФП вся преамбула мутабельных действий над объектом — это просто инициализация. И пришлось бы сказать, что ООП допустимо в инициализации аргументов, что довольно узко.
nlinker Автор
Ну хорошо, я рад, что мы понимаем друг друга :-)
EvgeniiR
Только ООП в том определении в котором этот термин появился ничего общего с этими идеями не имеет.
См. —
wiki.c2.com/?AlanKaysDefinitionOfObjectOriented и www.youtube.com/watch?v=fhOHn9TClXY
P.S. Все лекции по ООП которые называют наследование одним из главных принципов — только вредят.
netricks
Современное ООП сильно отличается от идей Алана Кея… Да, отличается.
Вообще, изначальная формулировка, данная господином Кеем имеет скорее историческое значение.
И да, наследование оказалось не очень удачной концепцией. Но методология парадигмы ООП не статична. Она развивается и изменяется. Это нормально.
EvgeniiR
Современное ООП это чёрти-что без внятного определения, и гораздо полезнее будет изучать что-то более приземлённое.
netricks
Парадигма и не должна иметь внятного определения, поскольку она принцип… Это способ взгляда на мир. Эта та картина мира, которая существует в голове программиста, когда он пишет код. Это что-то вроде инженерного мировоззрения и явление это даже более культурное, чем техническое.
Строго говоря, есть всего одна парадигма программирования, которая имеет что-то вроде определения — структурное программирование. Структурное программирование оказалось очень простым и очень успешным. Настолько, что теперь мы все им пользуемся, а принцип структурного программирования стал самоочевидным.
Я вообще полагаю, что изучение парадигм программирования — это довольно высокий уровень. парадигмы где-то на одном уровне с шаблонами проектирования, если не выше. На первых этапах обучения кодерскому искусству изучать парадигмы, как мне кажется, скорее вредно, чем полезно. Это как пытаться разобраться с причинами первой мировой войны, не разбираясь в экономике, психологии, социологии и куче других дисциплин… Ну то есть, можно, но толку особого не будет.
Но мы почему-то считаем, что ООП нужно давать студентам…
EvgeniiR
В целом я согласен про парадигмы и студентов. Больше всего я не люблю необоснованный догматизм вокруг этого определения.
Но существование какой-то картины в голове мне не ясно. Звучит как магия.
netricks
Ну… Это довольно сложная и нестандартная тема…
Я бы сказал, что это и ощущается как магия. Если мы заглянем немного дальше постановки вопроса постижения принципа как культурного явления, мы обнаружим, что программист, как впрочем, и любой представитель творческой профессии, достигая определённого уровня понимания принципа внезапно обнаруживает себя в ситуации отсутствия выбора. Он начинает писать код согласно требованиям принципа и ощущению эстетической красоты. И внезапно оказывается, что не он пишет код, но как бы код сам пишет себя… И это реально ощущается так, как будто кто-то надиктовывает текст программы прямо в мозг.
Но эта тема выходит далеко за рамки технической стороны программирования и затрагивает особенности явления искусства в целом.
Принцип кодирует огромное количество информации и не может быть использован вне культурного кода… Это не набор рекомендаций относительно того, как рыть тунель, но скорее эстетическое представление о том, как рыть тунель. А поскольку эстетическое представление неформализуемо, то оно конечно ощущается магией.
Чтобы научить человека принципу, его надо ввести в культуру и оставить его в этой культуре на какое-то время. Потому что культуре нельзя научить. Ее можно только дать впитать, через тексты представителей культуры, через общение с представителями культуры. Чтобы понять принцип, нужно смотреть на то, как представители культуры действуют, на то, как они думают, на то, как они приходят к своим решениям. И это несколько отличается от обучения конкретным методам. Хотя методы, конечно, тоже являются порождениями культуры. Самым, так сказать, явным её слоем.
Но это всё… Немного за границами настоящего обсуждения.
PsyHaSTe
Разве? Я вполне удовлетворен определением, что программа написана в ФП стиле если она состоит из чистых функций, тогда как
netricks
Я безусловно согласен с тем, что среди всех остальных парадигм ФП наиболее похожа на законченную концепцию.
Но мне кажется сообщество не до конца привыкло к функциональному программированию и некоторые дебаты вокруг него еще продолжаются.
Думаю лет через десять можно будет уже сказать точно.
PsyHaSTe
Ну мне кажется, что для привыкания сообщества в первую очередь стоит мифы развеивать, чем я по мере сил и стараюсь заниматься. А то люди навыдумывают себе что ФП это про то чтобы описать типы в типах, и сидеть довольными в своей башне из слоновой кости.
Я ведь прошел обычный путь разработки. Паскаль/Си, Дельфи, Сишарп, на котором и остановился надолго. Думал начать учить ФП, но споткнулся об "моноиды в категории эндофункторов", испугался, и не стал. А оказалось, что там все просто как дышать, и те же солиды или даже банда четырёх куда сложнее.
netricks
Ну чтож. С цель развеивания мифов и популяризации ФП, так и запишем. СУЩЕСТВУЮТ ДВЕ ПАРАДИГМЫ ПРОГРАММИРОВАНИЯ, КОТОРЫЕ ИМЕЮТ ЧТО-ТО ВРОДЕ ОПРЕДЕЛЕНИЯ. Пусть все услышат и пусть отныне будет так.
:-)
Vlad800
Я бы сказал, что ООП декомпозирует статику (картину мира), а ФП действия (алгоритмы работы с потоками данных). И кмк вся проблема в том, что «религиозность» мешает создать правильный их сплав.
upd
S-e-n
Во-вторых: в ООП работа с данными, имеющими действия происходит только когда у методов вообще нет параметров. Если у методов есть параметры, получается работа с данными, имеющими действия, которые в свою очередь имеют данные. Если говорить в ваших терминах.
netricks
Ну… Я пожалуй просто сошлюсь на книжку «Исскуство програмирования для Unix», на которую я уже ссылался в этом треде. Там где-то было достаточно подробное рассмотрение того, почему данные требуют внимания больше, чем алгоритмы.
AnthonyMikh
Что-то я не понимаю, каким образом в этом списке оказалось наследование. Оно же, наоборот, очень сильную связь создаёт.
netricks
Идей у наследования две.
Первая — вынести и переиспользовать часть кода классов кодирующих схожие объекты.
Вторая — идея интерфейса — использовать родительский тип как интерфейс к многим разным вариантам имплементации.
Впоследствии идеи разнесли в концепции миксинов и интерфейсов… Или кто там их как называет, поскольку совместно они работали не очень хорошо.
Проблема наследования в том, что оно стимулирует программиста писать довольно большие иерархии классов, тем самым порождая сильную связность, однако идеи положенные в основу наследования по прежнему актуальны. Ждем более удачные реализации.
AnthonyMikh
Это вполне реализуется композицией, которую вполне можно было бы использовать ещё в C.
Это вполне решается классами типов в Haskell (1990). Или, если копнуть глубже, модулями в Standard ML (1983).
Так что здоровые альтернативы наследованию существуют уже достаточно давно.
netricks
И тем не менее наследование инспирировано именно идеями декомпозиции. Было бы довольно неблагодарно и недальновидно забыть о нем, в вопросе происхождения ООП. Мы же не обсуждаем, применять его или нет, но пытаемся понять, что такое ООП в целом.
0xd34df00d
То есть, когда я пишу на хаскеле, разбиваю программу на модули, инкапсулирую какие-то вещи в непрозрачные типы, у которых не экспортируются конструкторы и аксессоры, использую такие средства, как тайпклассы и экзистенциальные типы, то у меня получается ООП?
netricks
А я вам больше скажу… Довольно сложно писать функционально и при этом необъектно.
Vlad800
А здесь не путаются понятия «акторы» (более широкое определение) и «объекты» (узкое)?
Gargo
в Swift строгая статическая типизация. Поработав с ним с уверенностью скажу — лучше бы она была динамическая и нестрогая. Костылей наворотили выше крыши. Один только отдельный тип для substring чего стоит
mkll
Мне всё нравится, мне optionals не нравятся. И двойственность — то, что категорически запрещено в рамках стандартной библиотеки, легко дозволяется в UIKit, поскольку он на деле ObjC в свифтовой обертке.
rboots
Для меня, и думаю для большого числа других разработчиков, ценность динамической типизации заключается всего лишь в том, что нужно писать меньше кода. Да, на статических типах можно написать систему любого уровня сложности, но придётся везде пробрасывать типы, создавать не всегда нужные зависимости на них и т.п… К сожалению реальность такова, что я сам, как сторонник динамической типизации, в реальных проектах часто применяю TypeScript, так как:
— реально слабопрогнозируемые системы приходится разрабатывать редко
— типы спасают от ошибок по невнимательности, пусть и ценой лишнего кода
— и главная причина: от ошибок по невнимательности не спасает IDE. К сожалению, в 2020 году, когда машины распознают дорожную разметку и пешеходов, IDE всё ещё не способны распознать тип переменной в программе. Я убеждён, что нет причины для программиста писать Int или String на каждое действие, всё это можно делегировать машине. Динамическая типизация это прекрасная высокоуровневая концепция, но инструменты для JavaScript в наше время находятся на уровне ниже, чем инструменты .NET в 2010м. В итоге получается прекрасная технология, к которой приходится добавлять сурогаты из за того, что машинам слишком сложно в ней работать.
PsyHaSTe
По моему опыту как раз при динамической типизации писать приходится больше. Потому что приходится тестировать то, что в статических япах проверял компилятор. А декларативное описание почти всегда короче и точнее, чем набор тестов.
foo(a: int) -> int
намного проще и короче написать, чем тесты, которые валидируют, что функция принимает только инты и возвращает только инты.AlexBin
Нет, не приходится. Уже давно есть аннотации, которые решают вашу проблему на этапе написания, а не рантайма.
Таким образом достигается комбо преимуществ статической и динамической типизаций. Аннотациями можно полностью покрывать швы. А в локальных участках, где срок жизни какой-то переменной составляет 5 строчек, типизация почти никогда не требуется, особенно, если использовать правильные названия идентификаторов типа attempts_count (попробуете угадать тип?). И даже если по имени тип непонятен, просто поднимите взгляд на пару сантиметров вверх (это на крайний случай). А уж если вы хотите попыткам добавить разных таймаутов, то попробуйте вот это переписать на сишарп (вы же на нем пишете?):
Казалось бы, это все мелочи, но их такое огромное количество, что из них состоит половина кода, что мы пишем.
Кроме того, в такой комбо-типизации значительно упрощается прототипирование. Я могу хоть 10 раз переписать какой-то новый для меня механизм в ходе изучения, а уже готовому варианту расставить типы.
Сам я начинал со статической, и потом несколько раз пытался на нее вернуться, потому что мне реально ее не хватало. Но аннотации меня спасли, и мне стало даже лучше.
И прежде, чем со мной спорить, попробуйте меня понять, вы же видите, что я не фанатик, и не делю мир на черное и белое.
0xd34df00d
Которые есть типы, прикрученные сбоку к языкам, которые изначально не были задизайнены с учётом такой возможности (что в итоге всегда получается плохо). В чём выигрыш?
У вас, кажется, ложная дихотомия. В локальных участках и на статически типизированных языках аннотации типов совсем не обязательны, компилятор их часто может вывести.
rboots
Получается, вроде все согласны, что:
— в локальных участках типизация часто не нужна и можно де-факто использовать динамическую даже в статически типизованных языках
— при взаимодействии большого числа сложных модулей типизация полезна, хотя бы даже как документация, в виде типов, аннотаций или розолвинга типов от IDE (наступит же когда-нибудь это светлое будущее)
Получается спор об одном и том же, разница только в том, какой подход использовать по дефолту.
0xd34df00d
Не надо путать неявные аннотации (при статической типизации) с динамической типизацией.
rboots
Просветите какую вы видите разницу? Я вижу только ту, что в некоторых языках нельзя менять тип переменной, даже объявленной неявно, что не выглядит существенным в данном обсуждении.
0xd34df00d
Разница в том, получите ли вы ошибку в рантайме или в компилтайме, если вы написали ерунду.
Я могу на хаскеле написать код вроде
и получить ошибку сразу, не запуская его:
хотя тут нет ни единой аннотации типов.
Если я аналогичное напишу на каком-нибудь питоне, то ошибку получу исключительно во время выполнения.
rboots
Во фронтенде одна из самых распространённых операций — это приведение чего-нибудь к строке, чтобы вывести пользователю, поэтому в JavaScript и сделано приведение типов автоматическим.
Хотя в том же C++, насколько я помню, ещё интересней и перегрузка операторов позволяет определить применение любых операторов к любым парам типов, хотя типизация там вполне статическая. Так что не сказал бы, что эта особенность языков жёстко связана с динамической или статической типизацией.
PsyHaSTe
Перегрузка операторов есть и в расте, поэтому там можно складывать строки с числами, и он скастует автоматически (если написать соответствующий ньютайп).
Но вот складывать орехи с массивами байт случайно не получится даже в таком случае.
Ну и да, я не думаю что преобразования в строку в JS встречается чаще, чем в любом десктопном UI-фреймворке, но там без этого отлично получается жить. Впрочем, и в JS насколько мне известно это не так часто нужно, потому что там данные-отдельно, а их рендерер — отдельно, и он вполне может отрисовывать не только строчки.
Kanut
Кстати именно динамическое преобразование «чего-то» в строку работает много где. То есть если вы конкатенируете какой-то объект со строкой, то на объекте «имплицитно» вызывается какой-нибудь ToString().
PsyHaSTe
То что в расте трейты Debug и Display вынесены, и главное не реализованы для типов для которых не имеют смысла это просто манна небесная.
Я буквально на прошлой неделе правил в сишарпе место которое в лог писало
[object Object]
потому что человек тустринг переопределить забыл, и вместо сообщения об ошибке писалось очень полезноеMy.Company.Name.UsefulException'2
.Kanut
Бывает. Но такое в принципе можно перетерпеть и на мой взгляд именно со строками польза перевешивает вред.
С другой стороны то что в JS творится это на мой взгляд уже явный перебор.
0xd34df00d
Особенно
undefined
или[Object]
.А вообще ad-hoc-полиморфизм в C++ (умное название для перегрузки функций) не имеет никакого отношения к неявным приведениям, а неявные приведения не имеют никакого отношения к статичности типизации.
Впрочем, как бы вы делали ad-hoc polymoprhism без статических типов, я не знаю.
Shatun
Статическая типизация — это значит что тип известен и он не меняется. Его необаязательно задавать, посмотрите например на var в java-там нету динмаической типизации, но есть var
var или int использовать — это спор о явной и неявной типизации.
Автоматическое приведение типов этого также не отменяет(опять же java -хороший пример)
В целом же разговор в статье идет именно про статическую vs динамическую систему типов.
TheShock
Чем это хуже JS?
6opoDuJIo
Вы путаете неявную аннотацию и динамическую типизацию. Здесь тип выводится на этапе компиляции, и после того, как он был выведен, работает строгая типизация. На c# вы не можете написать:
TheShock
Я ничего не путаю.
Зачем такое писать я не знаю? Даже в JS такое говно не пропустят линтеры.
Я спрашиваю, про код на практике.
6opoDuJIo
Всё вы путаете, и я не понимаю чего вы добиваетесь и что пытаетесь доказать.
Повторю ещё раз: var не попадает в сборку, он существует только в тексте и, по итогу, в сборку попадает тип, который додумал компилятор. Он статичен, и не меняется во время выполнения.
TheShock
Ну? И что? Как это влияет на программиста? От того, что там внутри компилятор что-то додумал — вам внезапно хуже программировать стало?
Я отвечал на конкретный тезис:
Я считаю, что неявная аннотация для таких случаев прекрасно подходит.
6opoDuJIo
Действительно, какая разница, сижу я на кровати или пью ли чай. И то и то по кайфу.
Я пытаюсь сказать, что строгая типизация за неявной аннотацией всё ещё строгая. Вы пытаетесь поставить ненужную параллель с динамической только из-за того, что вам не нужно писать такое:
TheShock
В динамических языках изменение типа переменной считается дурным тоном и, обычно, не пропускается линтером. Так зачем это ставить как аргумент?
6opoDuJIo
Тут речь идёт чисто о терминологии, а не о том, что вы пропускаете динамический код через линтер.
TheShock
Вы влезли в мой комментарий к другому человеку и стараетесь мне рассказать, о чём была речь?
6opoDuJIo
Я пытаюсь вам сказать о том, что вы сравниваете несравнимое и путаете понятия. Либо же вы не так выразили мысль.
PsyHaSTe
Вполне можете:
Правда у меня один вопрос — зачем?
AlexBin
2. Выигрыш был описан ниже сразу после этих слов.
И линтеры динамических языков часто сами могут вывести типы в локальных участках. Только причем тут это? Мы не о линтерах, а об идее рассуждаем.
Я сторонник идеи что аннотации превращают динамический язык в статический в нужных местах и не превращают в ненужных. Если вы противопоставляете этой идее свою идею, что в статическом языке тоже можно скрыть типы в локальных участках, то я совершенно не против такого механизма. Я думаю, он хорош.
Если есть статический язык, в котором статичность везде опциональна, а не только в локальном контексте, я только за, потому что хотелось бы самому решать.
0xd34df00d
Я ради интереса попробовал сделать что-то с mypy. Если выходить за границы того, что даёт сишная система типов (а даёт она очень мало), то всё ломается и разваливается.
И работе над type hints в питоне уже вроде как много лет, а даже куча библиотек из «включённых батареек» всё ещё неаннотированна.
Ну и да, не питоном единым. В тот же хаскель вот тоже пытаются завести существенное усиление типизации (зависимые типы), а язык был изначально задизайнен без их учёта, и в итоге всё получается очень больно и уродливо.
Тут что ли тоже путаница между неявными аннотациями типов и отсутствием статических типов?
От того, что вы не выписываете тип каждой переменной, он не пропадает, и тайпчекер всё равно их проверяет.
AlexBin
И в этом виновата конечно же динамическая типизация? Вы же понимаете, что в случае со статической типизацией, к каждому программисту приставлен человек с пушкой, который следит, чтобы тот везде расставил типы. Если к динамическим программистам приставить такого же наблюдателя, они бы тоже расставили везде типы. То есть ваш тезис в том, что все дело в необходимости контроля ленивых программистов, да?
Это проблема не динамической типизации, а дисциплины. Динамическая типизация позволяет хулиганить, статическая — нет. И из этого нельзя делать ложные выводы, что ВСЕ динамические программисты хулиганы, а ВСЕ статические программисты — молодцы. С таким же успехом можно задвинуть глупый вывод, что статические программисты без ошибок на этапе компиляции тоже бы хулиганили, забивая на типы. Но это неправда, и мы таких глупых выводов делать не станем. Давайте не будем делить мир на черное и белое и соскакивать на другие темы.
Это у вас путаница между тем, что что важно компилятору, и тем, что важно программисту. Мы тут рассматриваем то, с чем приходится работать лично мне, а не компилятору. А мне приходится работать с кодом. Статический это код под капотом или динамический, мне по барабану, если он позволяет в одних местах делать все строго и статично, а в других свободно и динамично.
Чтобы вы перестали скатываться в другие темы, я еще раз хочу напомнить, что говорю я не про конкретные реализации чего-либо, а про саму идею опциональной типизации с точки зрения взгляда программиста. Вам сама идея не нравится? Или в чем вы меня пытаетесь убедить?
Мне по наследству достался прекрасный проект, обильно обмазанный динамичностью и полным отсутствием типов. Вплоть до того, что объекты одного семейства имели разные наборы методов. Сейчас я все это рефакторю, знаете какой это ад? Я чувствую себя настоящим героем. Но почему-то вы не можете поверить, что по мере рефакторинга код становится читаемым, в среде появляются подсказочки, а в рантайме перестают вываливаться ошибки, что метод отсутствует или переменная не определена.
0xd34df00d
Нет, не Рабинович напел. Исключительно личный опыт. Я бы им даже поделился, дабы вы могли оценить, сколько в нём объективного, а сколько — субъективного, если бы это было чуть менее давно, и я помнил чуть больше деталей.
Вы ведь понимаете, что линтеры — это такой маленький кусочек статической типизации, и они работают (ну, вернее, пытаются работать) точно так же, как статические тайпчекеры?
Поэтому говорить о том, что динамическая типизация норм, потому что есть линтеры — это как-то ну очень иронично получается.
Именно. И это прямо следует из того, что вы пишете дальше.
Нет, не понимаю, потому что это не так. Не нужно там везде расставлять типы.
У меня в коде есть только аннотации top-level-функций или биндингов, и я почти никогда не пишу аннотации типов локальных для top-level-функций вещей. Более того, в значимой части скучного кода (где я не пытаюсь выпендриваться на уровне типов) я мог бы стереть и аннотации самих функций, и компилятор бы их за меня вывел и проверил.
Нет, мой тезис в том, что статические типы нужны и полезны, а динамические метки и их проверки (почти всегда) не нужны и неполезны (потому что бьют по производительности, как минимум).
То есть, понятно, что можно взять питон, начать с ним героически воевать, расставить статические типы, написать их для всех используемых библиотек, прогнать mypy и потом радоваться профиту от кусочков статической типизации. Но зачем, если можно сразу взять нормальный статически типизированный язык?
Про качества программистов я вообще не говорил, мне это не очень интересно. Мне лишь интересно, сколько у меня головной боли при работе с кодом. И вот почему-то при работе со статически типизированным кодом её меньше.
Что значит динамично?
Вот этот код статический или динамический?
Во-первых, мы тут обсуждали статическую типизацию против динамической. Вещи вроде gradual typing, optional typing и тому подобное — другой вопрос. Ну да ладно.
Во-вторых, да, эта идея мне тоже не нравится. Идея не писать самому руками типы всех термов в моём коде мне нравится. Идея, что в моём коде будут существовать термы, тип которых компилятору неизвестен и им не проверяется, мне не нравится.
PsyHaSTe
А чем они отличаются от типов?
Скорее минусы обеих. Посмотрите на драму с актиксом, она это наглядно показывает. Как только какие-то жалкие 5-10% кода начинают забивать на типы, то вся программа разваливается как карточный домик. Поэтому нет, any через any с опциональным декорированием типов — это не "лучшее из двух миров".
Пожалуйста
Хоть сишарп далеко не эталон статически типизированных япов, но даже на нем не вижу особой проблемы, по крайней мере в этом примере. Тут подробно рассказано почему много писать типов не надо. Это трудно замерить, но по моим прикидкам типы едва ли 10-20% кода, потому что они участвуют только в топ декларациях: сигнатурах публичных функций (для приватных можно забить) и публичных же структур (вместо приватных можно перекидываться кортежами, например).
А мне наборот с типами проектировать проще, потому что я могу построить всю систему до того, как напишу хотя бы одну строчку кода. У меня будут функции вызывающие функции (с телом foo = undefined), и когда типы сойдутся и буду доволен апи модуля, то можно идти и заменять undefined на актуальные реализации.
Я не спорю с вашей точкой зрения) Я всего лишь разъясняю свою позицию, то что мне кажется вы переоцениваете стоимость поддержания типов, и наделяете их свойствами (например, сложность прототипирования), которыми они не обязательно обладают.
AlexBin
Что есть актикс? Ничего не понял. Не могу ответить.
Ну а дальше по вашему тексту, я думаю мы спорим ни о чем. Вы доказываете, что зебра черная в белую полоску красивее, чем белая в черную полоску. Вы меня ни в чем не убедили, я и так всегда считал важным наличие типизации, просто раньше когда я писал на статических языках, возможно типы нужно было указывать везде. Теперь я знаю, что в статических языках стало попроще с типами. Но так же я знаю, что и в динамических стало получше с аннотациями.
Благодаря этому, роль статической типизации размывается, и мы можем позволить себе выбирать язык по другим факторам, расширяя круг поиска.
Crandel
Нет, это так не работает. Нельзя что-то доказать наполовину. Если взять пример питона, то интерпретатору пофиг на них, для него их не существует. Вы легко передадите строку вместо инта и если там не будет интоспецифических методов, ваш код будет работать и не пикнет даже
AlexBin
Только не надо говорить, что проще взять сразу статический язык, я на это уже тут несколько раз отвечал.
0xd34df00d
О, к слову о силе статической типизации питона.
Я ожидаю от сервера число и два массива длины, соответствующей этому числу. Как будет выглядеть аннотация, гарантирующая это?
Cerberuser
А как будет выглядеть соответствующая аннотация вообще в любом языке без завтипов (т.е. практически в любом из тех, которые применяются сейчас, даже статически типизированных)? В лучшем случае у нас будет обобщённый callback, который принимает что-то типа GenericArray.
0xd34df00d
Без завтипов да, плохо будет.
Хотя конкретно для этого даже хаскелевской пародии на завтипы хватит, хотя это будет выглядеть настолько стрёмно, что писать целиком я это, конечно, не буду, но там будет что-то вроде
gBear
Тут главное не забывать, что при динамической типизации (диспетчеризация-то в runtime) для этого вообще «типы» могут и не понадобиться.
Например, можно вспомнить про guard'ы в erlang, какие-нибудь :-)
0xd34df00d
Они и при статической-то типизации нужны лишь для того, чтобы гарантировать валидированность данных. Не нужно её гарантировать — не используете всю эту наркоманию.
Только вот если гарантировать таки охота, то без статической типизации никуда. Динамические проверки не проверяются компилятором, и компилятор не ругнётся, если вы их где-то забыли.
gBear
«Гарантии» они как бы больше про сильную и слабую типизации, а не про статическую. Если типизация слабая, то какой бы статической она не была, то гарантировать там мало чего можно.
Ровно как и при сильной динамической типизации — никто не запрещает получить весь набор «гарантий», что и при сильной статической.
Ну а то, что эти «гарантии» давать будет не компилятор… ну и что с того?! Это, имхо, вообще не принципиально.
Kanut
Ну в теории в языках вроде джавы/сишарпа вы можете дефинировать новый тип/класс/интерфейс, который «наружу» будет выглядеть именно так как описано выше.
Или я что-то неправильно понимаю в вопросе?
0xd34df00d
Давайте начнём с такого вопроса: как там будет выглядеть массив данной длины (не фиксированной в компилтайме)?
Kanut
Что там запихать под капот это уже отдельный вопрос. И это на мой взгляд зависит от того для чего нам это надо.
А для нового интерфейса по идее можно вообще что угодно самому напридумывать.
П.С. У меня всё больше и больше впечатление что мы вроде как бы исползуем одни и теже термины, но говорим о немного разных вещах…
0xd34df00d
А тут оно не под капотом, оно прям кишками наружу торчит.
Вы, вероятно, говорите о чём-то вроде
или о другом?
Kanut
Вот даже не знаю как объяснить. То что вы написали это уже конкретная реализация, а я теперь уже пытаюсь придумать можно ли дефинировать контракт/интерфейс, который бы «работал» вне зависимости от того что там запихают в реализацию. Но похоже я просто что-то размечтался :)
А так в том же сишарпе вроде бы есть массивы/спискм с фиксированной длиной и можно попробовать как-то с ними поиграться.
0xd34df00d
Он, скорее всего, либо фиксирован во время компиляции, либо не торчит в виде куска типа.
Kanut
ArrayList с FixedSize вроде бы можно и во время выполнения создавать.
0xd34df00d
Ну, его размера-то в типе не видно.
Crandel
А я вот не доверяю своему коду через полгода, не говоря уж про код со стороны. А компиляторы хоть и имеют баги, но полагаются на матиматические доказательства. Мне остается только за бизнес логику думать
AnthonyMikh
То есть мы тратим время на проверку типов и до написания программы, и во время её исполнения? Прям win-win какой-то.
MIKEk8
Тут главное отличие в том, что мы привязываем типы только там где это нужно. В языках с динамической типизацией зачастую код пишется (пишем — пробуем — правим — пишем), а в языках со статической типизацией (думаем — пишем — пишем = работает). Если вы чётко понимаете откуда что берётся и куда идёт, то вам несложно проставить нужные типы, а если у вас задача обработать 90% запросов более-менее корректно (потому, что партнёр не может даже проверить по XSD схеме свой файл перед отправкой) то строгая типизация вам не поможет.
Kanut
В данном контексте нет никакой разницы есть у вас статическая типизация или нет. Вы либо можете обработать запрос, либо не можете. Если вы можете обрабатывать «на 90% правильные запросы» без строгой типизации, то я вам абсолютно точно так же напишу решение со строгой типизацией, которое их будет обрабатывать. Вообще не проблема.
AlexBin
Kanut
А может наоборот меньше :)
Ну на мой взгляд строгая типизация жизнь как раз упрощает. Как минимум мне уж точно.
AlexBin
AlexBin
Зачем везде, если у вас аннотации?
AnthonyMikh
Тут возникает вопрос, насколько подробно нужно эти аннотации проверять: поверхностные проверки могут привести к неожиданному/нежелательному поведению, а полные проверки типов в рантайме могут на порядок-два замедлить исполнение программ.
AlexBin
Неподконтрольная среда — это чужая либа или внешнее API, в котором вы не можете наверняка узнать тип. Да, эта среда нетипизированная.
Я выше описал подробнее, что я имел ввиду.
vanxant
А зачем? Извините, без наезда, но, похоже, вы просто слишком упоролись по статике.
Функции, обычно, нужно, чтобы её аргумент квакал как утка и выглядел как утка. А что там у него внутри — живая утка, Скрудж МакДак, уточка для ванны или уткоподобный робот-убийца — функцию не интересует и, в общем, не должно интересовать для уменьшения связности.
Ладно бы ещё статическая проверка типов действительно предотвращала большой процент ошибок. Но она совсем не всесильна. Выше в комментах я вас спрашивал про фазу Луны, которую вы аккуратно «забыли»; не зря спрашивал, потому что эта информация недоступна на этапе компиляции и не может быть, соответственно, проверена системой типов.
PsyHaSTe
Затем, чтобы быть уверенным в исходе событий. Потому что если я пишу функцию умножающие числа я не знаю как она себя поведет при умножении килограммов на мандарины. И самое главное, я не хочу об этом думать. Поэтому я говорю "я написал функцию для чисел, а если у тебя не число — не пользуйся ей". Запоминать 547 правил неявных преобразований и чему равно
[] + []
я не хочу.Верно, только вы без типов не знаете, пришло ли что-то уткоподобное или какой-нибудь медведь.
Идрис может статически проверять длины массивов которые считываются с консоли/из бд/по сети или еще откуда-то. Вы явно недооцениваете системы типов. Но даже без идриса это можно делать, просто для более мейнстримных языков будет другой трейдов "записать в типах"/"написать тест".
arthuriantech
Умножение килограммов на мандарины это проблема слабой типизации и неявных преобразований, а не проблема динамической типизации как таковой. В JavaScript можно сделать
[] * "abcdef"
, но Python этого не разрешит.0xd34df00d
Вопрос в том, не разрешит это язык во время написания кода или во время его выполнения. И проблема динамической
типизациипроверки меток в том, что во время написания кода она это не поймает.PsyHaSTe
Тут интересный вопрос, что означает это слово.
Лично для меня "разрешит" означает "прошло цикл деплоя в CI". То что оно там потом в рантайме упадет мне уже не поможет, если я разлил по клиентам новую версию софтины и у них вдруг котики пропали.
KvanTTT
Все дело в производительности — IDE должна работать без лагов, реактивно реагировать на действия пользователя.
ip1981
Нет никакой динамической типизации, нет никакой статической типизации. Есть только проверка корректности программы: на этапе выполнения (в том числе тестами) — "динамическая" типизации, или на этапе компиляции — "статическая". В этом смысле языки с "динамической" типизацией можно назвать более низкоуровневыми.
LireinCore
Мне показалось, что статья больше про строгая vs нестрогая, чем про статическая vs динамическая. В этом плане многие динамические языки сейчас уже сильно сместились в сторону строгости.
nlinker Автор
Это ведь термины, "статическая" и "динамическая" типизация, они ровно это и означают — есть отдельная стадия typecheck в компиляторе или нет.
Можно дальше рассуждать, насколько глубокая проверка типов, должны ли быть типы известны до этой проверки, насколько широкий класс проверок покрывают типы...
Так что вынужден не согласиться: данная стадия компиляции или существует, или нет. И типизация тоже существует. :-)