• паста types The so-called untyped (that is “dynamically typed”) languages are, in fact, unityped.  Rather than have a variety of types from which to choose, there is but one! © existentialtype.wordpress.com

    Несмотря на то, что профессор глаголит истину это не отменяет того факта, что статически типизированные языки не позволяют создать такой unitype включающий в себя все возможные представления данных, а это может быть чертовски неприятно при работе с динамически появляющимися разнородными данными.
    ♡ recommended by @jtootf, @O01eg

Replies (65)

  • @Kim, О! Не одному мне не нарвится идиотская связь тип == класс == интерфейс == поведение в мейнстрим ООП.
  • @Kim, В haskell, например, есть Dynamic, но он не позволяет работать с полиморфными функциями, что убивает пользу от него на корню
  • @Kim, Видимо я совсем отупел со своими хроматографами и микроконтроллерами. Приведите пожалуйста пример динамически появляющихся разнородных данных.
  • @Kim, /me видимо тупой и не понимает почему, не позволяют, для ссылочных типов есть корневой тип, можно работать с ним.. правда такая необходимость непонятно, когда возникает..
  • @ndtimofeev, Например REPL какого-либо языка. В программу вводят произвольно большое число данных, а при работе функции вида

    loop world = do
    data = readObject
    world' = process world data
    loop world'

    в type-safety языках мы не можем определить readObject так, чтобы он возвращал объекты разного типа в зависимости от данных. Конечно это обходится dynamic'ами (это типы состоящие из указателя на произвольные данные и метки типа), но там возникает лютый оверхэд при обработке. Естественное решение это создать
    datatype Object = I Int | D Double | S String | Struct String [(Name, Object)] | ...
    И ещё десятка полтора реализаций алгебраического типа Object, но это именно то, что названо в топике unitype и возникают проблемы подобные проблемам с динамиками (хоть и меньше) упомянутые в /2.
  • @qnikst, Корневой тип для ссылочных? Вы эти свои деревья наследований бросьте.
  • @Kim, interface TypeThatCanBeInReadObject {},
    остаётся только добавить его ко всем объектам, которые там могут встречаться (проблему приметивных типов, к сожалению тут не решить), а так же проблему final классов, которые не обладают описанным интерфейсом. Где тут деревья я в упор не вижу.

    З.Ы. адекватным этот метод не назовёшь конечно
  • @Kim, Как только в игру идут параметризованные типы всё разваливается. Число вариатнов начинает расти полиномиально
  • @qnikst, Хорошо. Я имел ввиду то же, что и /1
  • @Kim, ну /1 заведомо не верное утверждение..
    очень грубо говоря в языках с динамической мы имеем: абстрактный тип Container, с 2мя реализациями PrimitiveContainer и ReferenceContainer, где первый содержит ссылку на native страктуру данных с принитивным типом, а ReferenceContainer ссылку на Object, с дополнительными (специфичными для языка функциями) + описаны все динамические приведения типов, которых конечное кол-во.
    Так вот, я не понимаю, чем это принципиально отличается от мейнстримового ООП если мне кто объяснит — скажу спасибо
  • @qnikst, С таким подходом, в языках с "динамикой" мы имеем все и ничего. Вся информация о типе, классе, интерфейсах стерта до самого верхнего типа. Интересный вопрос, стоит ли разделять понятия класс и интерфейс. Я ответа на этот вопрос не знаю.
  • @Macil, Интересный вопрос на кой чёрт вообще вводить понятие класса и какого чёрта функции должны диспетчеризоваться ссылкой на объект? Уже это решение на корню убивает множественную диспетчеризацию в любом нормальном виде.
  • @Kim, Когда изучаешь хаскель вообше возникают сомнения в нужности ООП :) Множественная диспетчеризация — через алгебраические типы.Класс — носитель поведения, и видимо, интерфейса объекта.
  • @Macil, оффтоп уже, но моё мнение, на которое сильно повлиял преподаватель информатики
    физически: т.е. на програмном уровне интерфейс это подтип класса, т.е. класс с наложенными на него ограничениями, например можно сделать "интерфейсы" в плюсах, в которых такого понятия нет
    логически: их разделять стоит, т.к. классы и интерфейсы определяют разные подходы к наследованию, так интерфейсы определяют абстракцию на уровне поведения, а не реализации и приводят к другим решениям (как отмечается во многих ресурсах можно стравнить с типами в haskell)

    меньший оффтоп, как раз в динамике у нас есть все шансы сохранять доп информацию, которая в статике сотрётся
  • @Kim, такое ощущение, что или я нахожусь на существенно более низком уровне понимания, или опять не вижу глобальных проблем статических языков, кроме разве, что удобства программирования и дальнейшего расширения системы в качестве стат языка с ООП сравниваю с java
  • @Macil, Сомнения в нужности ООП возникают когда изучаешь хоть что-нибудь кроме ООП. Но я всё равно не понимаю что вы имеете ввиду этими странными аналогиями вроде "Класс — носитель поведения, и видимо, интерфейса объекта". Вы таки говорите не о типах, а о конкретно про ООП? Тогда человеческая реализация множественной диспетчеризации невозможна. Для её реализации в любом случае нужно разнести структуры и методы. А это уже не соответствует подходу используемому в современных класс-ориентированных языках.
  • @qnikst, разница в удобстве программирования это троекратная разница в количестве строк описания логики, а дальнейшее расширение системы в случае статики требует описания большого числа логики в одном месте, так что любое расширение системы — это фактически переписывание ядра.
  • @Kim, Ну и конечно глобальных проблем нет. Все используемые языки Тьюринг полны, а значит все проблемы только локальные и связаны с ограничениями конкретных языков.
  • @Kim, перед своим следующим вопросом я хотел бы уточнить, что с чем мы сравниваем, а то незаметно тут могло произойти смещение обсуждения
  • @qnikst, Сорри, но у меня парсер вылетел с эксцепшеном. Во-первых, ты путаешь понятие интерфейса с чисто абстрактными классами, определяемые черех ключевое слово interface. Во-вторых, поведение объекта не определяет ни что. Скажем так, принципы ООП предполагают некую гарантию того что объекты (и объектны наследников) класса Widget рисуют какую-то хрень на экране, а не форматируют жесткий диск, но и только. В-третьих, если отвязать класс от типа, то возникнут некоторые проблемы с наследованием и будет похерен субтиповый полиморфизм и скорее всего в язык схлопнется во что-то навроде Scheme с типами или SML.
  • @Kim, кстати, интересное поведение у статически типизированного go, в котором можно расширять поведение уже описанных структур данных (добавлять методы)
  • @qnikst, Языки с безопасной системой типов со статическими проверками безопасности типов и языки с динамическими проверками на ошибки связанные с неверным представлением об истинном типе данных.
  • @Kim, Заметь, тут ни слова нет про понятие объекта. Тут вопрос только в проверке соответствия типов при вызове функций.
  • @Kim, Единственная "нормальная" множественная диспетчеризация — на основе алгебраических типов. Все остальное — зло и я даже на эту тему говорить не хочу. А насчет того что я имею в виду. В традиционном ООП класс является носителем а) типа объекта б) интерфейса объекта в) поведения объекта. Мне это не нравится.
  • @Macil, 1). я не путаю интерфейс и полностью абстрактный класс :) такое ощущение, что у нас произошло, легкое непонимание друг друга.
    2). утв, что в ООП мейнстрип тип == класс, я оспаривать не буду, т.к. не считая некоторых замечаний оно, вроде как, верное
    3). "Во-вторых, поведение объекта не определяет ни что. Скажем так, принципы ООП предполагают некую гарантию того что объекты (и объектны наследников) класса Widget рисуют какую-то хрень на экране, а не форматируют жесткий диск, но и только." — ну раз уж и пошла такая пьянка, то и этого они не гарантируют
  • @Macil, А автору статьи по ссылке из топика не нравится вообще понятие ООП, просто потому что оно анти-модульно (за счёт размазывания функционала по дереву наследования) и анти-параллельно (проблемы с синхронизациями и изменяемыми состояниями). И я не вижу с какого перепоя вы напихали ООП в тему "статика vs динамика". Это вообще ортогональные понятия.
  • @Kim, моя вина. В тему это попало из-за утверждения, что динамика в статике при необходимости реализуется, вводом от 1 до 3х типов и правилами перевода их др в друга.
  • @qnikst, собствено обсуждение ООП пошло из-за упоминания слова объек в попытках сформулировать утверждение
  • @Kim, к слову, справедливости ради, размазывание по классам и изменяемые состояния, тоже не параллельные с ООП подходом вопросы
  • @Kim, Ну раз на то пошло, то динамическая типизация — вообще химера. Ну произошла у тебя ошибка типизации и дальше-то что? Единственный язык с правильным подходом — ерланг. В этом случае ошибочный процесс должен подохнуть как можно быстрее, свалив все на своих супервизоров. Другой правильный подход, структуры данных должны типизироваться статически, а принадлежность к классу определяться динамически. Насколько я понял именно он и изложен автором статьи. Но это тоже химера.
  • @qnikst, Наследование, как один из трёх столпов, ООП гарантирует антимодульность. А само понятие «объект» гарантирует наличие внутреннего состояния, так что изменяемые состояния также тесно связаны с самой природой объектов.
  • @Macil, да я плохой, я только сейчас читаю текст <<Dynamic typing is but a special case of static typing, one that limits, rather than liberates, one that shuts down opportunities, rather than opening up new vistas.>> — в общем-то моё начальное утверждение
  • @Macil, Произошла ошибка типизации — обработай также как любую другую ошибку. По хорошему нет никакой разницы между ошибкой, когда функция открытия файла не смогла отработать и ошибкой, когда функции сложения были переданы строки. А уж обрабатывать это в эрланговском let_it_fail, в коммон лисповом сигнальном протоколе или стиле пролога, где большинство ошибок просто значат, что предикат ложен.
  • @Kim, Хе, насколько я понимаю, ранними идеологами ООП класс считался отдельным модулем ;) Модульность — это прежде всего абстрактные типы данных. И в конце-коцнов ООП не так уж и плохо его обеспечивает.
  • @Kim, Наследование, как один из трёх столпов, ООП гарантирует антимодульность — наследование на основе интерфейсов, не создаёт антимодульности, наследование на основе интерфейсов отвечает принципам ООП.

    А само понятие «объект» гарантирует наличие внутреннего состояния — это ничем не отличается от любой структуры данных, опс т.е. отличается тем, что в объекте хранятся ссылки на методы для работы с этим объектом и ограничением на вид струтуры, который принят в данном ЯП, но это ничего не меняет

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

    я поверю в то, что эти утверждения справедливы для большей части ООП проектов и подходов, но я не согласен со справедливостью этих утверждений в целом.
  • @Kim, Только вот язык в этом тебе не поможет. Придется все делать самому.
  • @qnikst, А само понятие «объект» гарантирует наличие внутреннего состояния — это ничем не отличается от любой структуры данныхНу почему же не отличается? Есть чисто функциональные структуры данных, которые просто не изменяемы. Совсем. Никак.

    я поверю в то, что эти утверждения справедливы для большей части ООП проектов и подходов, но я не согласен со справедливостью этих утверждений в целом.Вы из тех, кто любит говорить, что если есть способ реализовать инкапсуляцию, то это ООП? Ну тогда тут не о чем говорить. Такие любители считают и хаскель ООП языком.
  • @qnikst, кстати в первом утверждении предполагается справедливость исходного тезиса о том, что размывание функционала по классам это антимодульность, что имхо в общем случае не верно
  • @Macil, Зато при этом язык может помочь с рядом других вещей, таких как динамическая диспетчеризация методов и легковестная реализация обхода разнотипных данных.
  • @Macil, помогут Higher-Order Contracts со стороны языка.
  • @tum, А например?
  • @Kim,
    Есть чисто функциональные структуры данных, которые просто не изменяемы. Совсем. Никак. ты так пишешь, как будто бы это не возможно в ООП языках. (предлагаю забить на обсуждение неизм структур в рамках данного обсуждения, т.к. тут опять же всплывает не мало вопросов)
    Вы из тех, кто любит говорить, что если есть способ реализовать инкапсуляцию, то это ООП? Ну тогда тут не о чем говорить. Такие любители считают и хаскель ООП языкомне вижу, как из моих слов следует это утверждение
  • @Kim, Алгебраические типы решают. Есть концепция Scrap Your Boilerplate позволяющая разделить логику обхода и логику вычисления результата обхода. Кроме того, предментая область конечна. Так что мы говорим о конечном числе сущностей.
  • @Kim, а можно всё таки, пример, обхода (и главное действий) над разнотипными типами данных
  • @qnikst, пример в начале обсуждения не приводить, или объяснить как будет работать readObject
  • @Macil, SYB это хак на динамиках, который, кроме всего, имеет проблемы с выразительностью. А алгебраические типы это просто типы. Они не могут решать проблемы логики. Ну и конечное число сущностей оно хоть и конечно, но любит расти экспоненциально, так что ты утонешь в служебном коде.
  • @qnikst, В статически типизированном языке как нибудь просто. На уровне вот этого хаскель-кода:

    readObject = getLine >>= return . readAtom

    readAtom :: String -> Dynamic
    readAtom ('i':xs) = toDyn (read xs :: Integer)
    readAtom ('d':xs) = toDyn (read xs :: Double)

    А потом использовать эти динамики вручную разруливая диспетчеризацию

    add :: Dynamic -> Dynamic -> Maybe Dynamic
    add a b
    | ((dynTypeRep a, dynTypeRep b) ==
    (typeOf (undefined::Integer), typeOf (undefined::Integer))) = do
    ar <- ((fromDynamic a)::(Maybe Integer))
    br <- ((fromDynamic b)::(Maybe Integer))
    Just $ toDyn $ (+) ar br else
    | ((dynTypeRep a, dynTypeRep b) ==
    (typeOf (undefined::Double), typeOf (undefined::Double))) = do
    ar <- ((fromDynamic a)::(Maybe Double))
    br <- ((fromDynamic b)::(Maybe Double))
    Just (toDyn (ar + br))
    | ((dynTypeRep a, dynTypeRep b) ==
    (typeOf (undefined::Integer), typeOf (undefined::Double))) = do
    ar <- ((fromDynamic a)::(Maybe Integer))
    br <- ((fromDynamic b)::(Maybe Double))
    Just (toDyn (((fromIntegral ar) + br)::Double))
    | ((dynTypeRep a, dynTypeRep b) ==
    (typeOf (undefined::Double), typeOf (undefined::Integer))) = do
    ar <- ((fromDynamic a)::(Maybe Double))
    br <- ((fromDynamic b)::(Maybe Integer))
    Just (toDyn ((ar + (fromIntegral br))::Double))
    | otherwise = Nothing
  • @Kim, в общем случае расти не будет, во всяком случае экспоненциально. Т.к. ООП^W подходы к проектированию учат выделять абстракции на уровне данных. У нас есть некий уровень абстракции, который может быть ADT (очень круто, тут я пасс), абстрактным объектом (абстракция по типу), интерфейсом (абстракция по поведению). Далее в функции мы уточняем какие типы нам подходят, с другими прогамма заведомо будет давать не верный результат. В последствии вводимые объекты обычно отвечают выбранной абстракции или у нас проблемы с проектированием. Исключение: изначально разнородная струтура, напр. в случае собственно изучения структур данных.
  • @Kim, Предметная область относительно стабильна... А вот в реализации. Если в предметной области "внезапно" обнаружилась еще одна сущность, то да, количество служебного кода может сильно вырости.
  • @Kim, А делает код-то что?
  • @Kim, Соответственно на каждую строку кода, для того чтобы обойти ограничения статики, до четырёх строк кода служебного. В случае динамической типизации это бы заменилось на одну строку служебного кода связанного с проверкой типов. В принципе троекратная экономия кода на месте.
  • @Macil, 2 числа складывает в нужных типах
  • @qnikst, Вспоминая классический пример про барабаны, барабанные палочки и метод sound(барабан, палочки) тебе всё равно потребуется количество_барабанов*количество_палочек реализаций. Даже эти такие случаи у меня возникали несколько раз. Тут при добавлении одной палочки в набор надо дописать методов по количеству барабанов.

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

    по поводу примера (покапитаню): sound реально зависит от размера, обивки, натяжки барабана, размер, состав, вид палочки, сила, место удара. Далее остаётся выделить в реализациях барабанов и палочек эти параметры, причём лучше методами, тогда реальных параметров может и не быть (деревянный "барабан" без натяжки).
    В случае смены физической модели — возникают проблемы :) нужно дополнить или переписать всё.
  • @qnikst, кстати я рад бы услышать варианты проектирования устойчивых к смене физ модели программ
  • @Macil, упоминание контрактов относилось к "Произошла ошибка типизации — обработай также как любую другую ошибку". Как-то так: у нас есть контракт чекер, работающий в рантайме, который проверяет предикаты, например, domain и range функции, и в случае фейла сигнализирует о contract violation, вешая blame на caller'a (если не срослось с domain) или на саму функцию, в случае с range'ем. Дальше не вижу препятствий к обработке blame.
    references
    eecs.northwestern.edu
    cs.brown.edu
    homepages.inf.ed.ac.uk
  • @Kim, за код спасибо, я обдумаю на досуге, решу что хочу сказать :) а пока у меня есть только мнение, что не на простейших типах в динамически типизируемых языках будет та же ситуация
  • @qnikst, На самом деле не будет. Тут же весь оверхэд по коду это проверки типов времени исполнения. В случае строгой динамической типизации они обязательно встраиваются в рантайм.
  • @Kim, в динамически типизированных языках мы имеем автоматически сгенерированный тип

    data Type = I Integer | D Double — | все остальные созданные типы

    А дальше тот же самый код

    readObject = getLine >>= return . readAtom

    readAtom :: String -> Type
    readAtom ('i':xs) = I $ read xs
    readAtom ('d':xs) = D $ read xs

    И отсутствие ручных проверок и кастов:

    add :: Type -> Type -> Maybe Type
    add (I a) (I b) = I $ a + b
    add (D a) (I b) = D $ (fromIntegral a) + b
    add (I a) (D b) = D $ a + (fromIntegral b)
    add (D a) (D b) = D $ a + b
    add = undefined

    Таким образом все проблемы волшебным образом исчезли. Но (в случае статического языка) появилась проблема ручного и централизованного описания Type.
  • @Kim, Видел я описание Printf в хацкеле, там что-то типа такого было.
  • @Kim, Простите, накосячил при переписывании. Если честно, то в том случае когда мы хотим обойтись без ошибок надо писать так:

    add :: Type -> Type -> Maybe Type
    add (I a) (I b) = Just $ I $ a + b
    add (D a) (I b) = Just $ D $ (fromIntegral a) + b
    add (I a) (D b) = Just $ D $ a + (fromIntegral b)
    add (D a) (D b) = Just $ D $ a + b
    add = Nothing
  • @tum, на практике, а не в research papers эти контракты могут выглядеть, например, так: docs.racket-lang.org (гайд) docs.racket-lang.org (референс).
  • @O01eg, printf на Template Haskell написан
  • @jtootf, так там, как только вышли из лексической области видимости этих переменных, тоже надо создавать при помощи unsafe кастов функции вида ((forall a. Any a) -> b) по одной для каждого b (если мы знаем в каком случае какой тип реаьно используется) , либо полность реализовывать те же Dynamic с сохранением метки типа и милым использованием результата полиморфной функции в её коде как в fromDynamic, либо (третий вариант использования и единственный адекватный) определать class со всеми операциями в системе и говорить (data Any = forall a. MegaClass a => Any a), но тут просто проблема просто переводится из определения типа данных содержащего все типы в определение класса содержащего все функции.