LINUX.ORG.RU

Модель типизации


0

0

Читаю доку по Python, возник вопрос: в чем преимущества нестрогой модели типизации перед строгой?

К примеру в Python'е возможность передать тот или иной объект в некоторую функцию определяется наличием в объекта "нужных" функции методов/полей. В то же время в C++ то же действие определяется иерархией классов объекта, что имхо логичнее, так как несет определенную семантику. Что я пропустил? :)

З.Ы. Не флейма ради...

З.З.Ы. Если ЖЖ - перенесите в Толкс пжлс.

anonymous

Динамическая типизая позволяет с меньшими трудозатратами в последствии переделывать код.

P.S. А так-же с меньшими трудозатратами порождать баги.

anonymous
()

Нестрогая типизация позволяет писать меньше кода. Теоретически, строгая типизация позволяет отлавливать некоторые классы ошибок на этапе компиляции. Практически, не помню кто сказал: "Код на Python настолько меньше аналогичного на C++, что те ошибки, с которыми в C++ борется строгая типизация, в Python-программе такого размера просто не возникают." (цитата не точная).

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

watashiwa_daredeska ★★★★
()

Вообще-то, ни в чём. Если есть нормальный вывод типов - статическая типизация на порядок лучше. Но вот C++ я бы как образец статической типизации приводить не стал.

Miguel ★★★★★
()

Принудительная строгая типизация - это почти всегда premature optimization.

grob ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

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

Почему? Потому что отсутствует необходимость явно приводить тип?

>Но в маленьких программках "для себя" она вредна.

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

anonymous
()
Ответ на: комментарий от watashiwa_daredeska

>"Код на Python настолько меньше аналогичного на C++, что те ошибки, с которыми в C++ борется строгая типизация, в Python-программе такого размера просто не возникают."

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

anonymous
()
Ответ на: комментарий от watashiwa_daredeska

С другой стороны, код на OCaml или Haskell ещё меньше и компактнее, чем аналогичный код на Python, при этом типизация существенно более строгая, чем во всяких разных C++.

anonymous
()

> в чем преимущества нестрогой модели типизации перед строгой?

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

> Что я пропустил?

Type inference, как тебе уже напомнили.

tailgunner ★★★★★
()
Ответ на: комментарий от anonymous

1) Гораздо строже. Например, в Це можно записать union как один тип, а прочитать как другой (по ошибке). В Haskell это невозможно, причём ошибка подобного сорта будет обнаружена при компиляции. 2) Гораздо мощнее. То, что в плюсах с трудом и с матюками делается на шаблонах, в Хаскеле делается легко и изящно; то, что в Хаскеле делается с трудом, в плюсах не делается. 3) Гораздо компактнее. Типы, как правило, писать на надо - компилятор выводит их сам.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

> в Це можно записать union как один тип, а прочитать как другой (по ошибке). В Haskell это невозможно, причём ошибка подобного сорта будет обнаружена при компиляции.

Каким образом?

> Типы, как правило, писать на надо

Скорее "не необходимо".

tailgunner ★★★★★
()
Ответ на: комментарий от anonymous

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

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

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от anonymous

> в Python'е просто много более высокоуровневых библиотек, потому и кода получается меньше.

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от anonymous

> код на OCaml или Haskell ещё меньше и компактнее, чем аналогичный код на Python

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от anonymous

> Почему? Потому что отсутствует необходимость явно приводить тип?

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

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

Преобразование типов --- фигня. Проблемы с созданием гетерогенных списков для меня более существенны.

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от tailgunner

> Каким образом?

Определяем union типов, скажем, Int, Char и String:

data MyUnion = M1 Int | M2 Char | Q String

Имена M1, M2 и Q взяты от балды.

После этого

x :: MyUnion -- явно указан тип, можно этого не делать.

x = M2 'z'

Теперь x хранит символ 'z'.

Использовать x как Int невозможно, на x+2 компилятор выдаст ошибку. Строго говоря, как Char тоже невозможно. Это другой тип. Можно сделать так, например:

processMyUnion (M1 n) = show n

processMyUnion (M2 c) = c : " qqq"

processMyUnion (Q s) = "ttt" ++ s

Заметь, компилятор сам поймёт, что тип processMyUnion - MyUnion -> String.

processMyUnion x

выдаст строку "z qqq".

Примерно так.

Miguel ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

> Потому что отсутствует необходимость его в явном виде указывать

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

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

Какие проблемы?

data ZZ = I Int | S String

myList = [I 1, S "xxx", I 16]

Тип myList - [ZZ], выведен автоматически.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

> Примерно так.

tagged union, всё это понятно. Непонятно другое - если допустима процедура, получающая как параметр именно значение типа MyUnion, то откуда _компилятору_ знать, какой именно там тег (== значение какого типа там хранится)? А если компилятор этого не знает, то уже на этапе выполнения может появиться ошибка.

Или процедуры с параметром MyUnion быть не может в принципе? А как насчет возвращения MyUnion процедурой ввода-вывода?

tailgunner ★★★★★
()
Ответ на: комментарий от Miguel

> Как раз Хаскельный вариант.

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от tailgunner

> tagged union, всё это понятно

Угу.

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

Miguel ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

А можно пример? Хоть чуть-чуть реальный?

А то ведь хаскельные типы много чего могут. Даже те, которые в Haskell 98. А не хватит - подключим existentials или ещё что-нибудь в этом духе.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

> если я хочу вытащить значение, лежащее в этом юнионе, я ОБЯЗАН специфицировать, какой тег там лежит.

Конечно, конечно. Но это не позволяет компилятору отловить _все_ ошибки, ведь так? На этап исполнения кое-что всё же останется.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

> На этап исполнения кое-что всё же останется. Естественно. Отловить ВСЕ ошибки... Не знаю, может быть, Epigram в состоянии. Хаскель - нет. Но ни при каком условии, никогда один подтип юниона не будет по ошибке принят за другой.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

> А можно пример? Хоть чуть-чуть реальный?

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от watashiwa_daredeska

>как библиотеку в общем виде (т.е. типы сообщений и их количество не известны)

Например так:

module Dispather where

send m outq = outq m

dispatch inq subs = do { m <- inq ; subs >>= mapM_ (send m) }

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

Пояснение на всякий случай: m - сообщение, inq - функция чтения входной очереди диспетчера (-> Monad m), subs - функция выбора списка подписчиков (m -> Monad [outq]), outq - функция записи во входной очередь получателя (m -> Monad ()). Всё независимо от реализаций обоих типов очередей, БД подписчиков, правил их выбора.

DonkeyHot ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

Если я правильно понял задачу, то existentials здесь - как раз в тему. Сам не пользуюсь, но выглядит это примерно так:

class Typeable a => IsMessage a

data Message = forall a. IsMessage a => a deriving Typeable

messageHandle :: IsMessage a => (a -> b) -> Message -> Maybe b

messageHandle handler msg = liftM handler $ cast msg

Функция messageHandle обрабатывает сообщение msg функцией handler, если msg имеет тип, понимаемый handler (и возвращает Just answer, где answer - ответ handler); в противном случае, возвращает Nothing. Использовать по вкусу.

В приложении объявляем типы сообщений

data MyMessage1 = ... deriving Typeable

data MyMessage2 = ... deriving Typeable

Объявляем, что они могут использоваться как сообщения

instance IsMessage MyMessage1

instance IsMessage MyMessage2

Но это уже где-то 0.3 олега, чаще всего можно сделать существенно проще.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

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

Конечно... просто я подумал, что Хаскел здесь ввел какой-то новый сверхмощный примитив, а это обычная проверка тега, этот оператор уже лет 30 наза изобретен :)

tailgunner ★★★★★
()
Ответ на: комментарий от Miguel

Сорри, чуть не так:

data Message = forall a . IsMessage a => Message a deriving Typeable

messageHandle handler (Message msg) = liftM handler $ cast msg

Miguel ★★★★★
()
Ответ на: комментарий от tailgunner

>я подумал, Хаскел здесь ввел какой-то новый ... а это обычная проверка тега,

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

DonkeyHot ★★★★★
()
Ответ на: комментарий от Miguel

> Если я правильно понял задачу, то existentials здесь - как раз в тему.

Попробую на досуге.

Вы с DonkeyHot'ом привели две разные части решения одной задачи. У меня только вопрос по твоему решению: как хранить handler'ы?

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от DonkeyHot

> Например так:

Замечательно, но я не понял, как осуществляется передача сообщения _только_ тем обработчикам, которые на него подписаны.

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от watashiwa_daredeska

Что значит "как хранить"?

handler - обычная функция, скажем,

MyHandler1 :: MyMessage1 -> какой тип захочется.

Miguel ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

> Вы с DonkeyHot'ом привели две разные части решения одной задачи.

Потому что мы, строго говоря, решали разные задачи.

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

Miguel ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

>как осуществляется передача сообщения _только_

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

>А что делать, если заранее не известны ни типы, ни их количество

"таким образом задача сводится к уже решённой".

DonkeyHot ★★★★★
()
Ответ на: комментарий от Miguel

> Почему обработчики надо выбирать именно по типу? Почему не по какому-нибудь тегу, например?

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от DonkeyHot

> Вопрос "как сравнивать типы сообщений" без конкретизации того, что такое "тип сообщения"

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

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от watashiwa_daredeska

Чтобы было понятнее, пример реализации на Python:

from __future__ import with_statement
from Queue import Queue
from threading import Thread, RLock

class Exit :
    pass

class EventDispatcher( Queue ) :
    def __init__( self ) :
        Queue.__init__( self )
        self.lock = RLock()
        self.handlers = {}

    def run( self ) :
        while 1 :
            event = self.get()
            if event == Exit :
                break
            with self.lock :
                for eclass, hlist in self.handlers.items() :
                    if isinstance( event, eclass ) :
                        for handler in hlist :
                            handler( event )

    def subscribe( self, eclass, handler ) :
        with self.lock :
            try:
                self.handlers[ eclass ].append( handler )
            except KeyError :
                self.handlers[ eclass ] = [ handler ]

    def unsubscribe( self, eclass, handler ) :
        with self.lock :
            try:
                self.handlers[ eclass ].remove( handler )
            except ( KeyError, ValueError ) :
                pass

Пример использования:

def intHandler( value ) :
    print "Int: %d" % (value,)

def strHandler( value ) :
    print "Str: %s" % (value,)

def anyHandler( value ) :
    print "Any: %r" % (value,)

dispatcher = EventDispatcher()
thread = Thread( target=dispatcher.run )
thread.start()

dispatcher.subscribe( int, intHandler )
dispatcher.subscribe( int, anyHandler )
dispatcher.subscribe( str, strHandler )
dispatcher.subscribe( str, anyHandler )

dispatcher.put( 5 )
dispatcher.put( "Test" )

dispatcher.put( Exit )
thread.join()

Вывод:
~$ python2.5 equeue.py
Int: 5
Any: 5
Str: Test
Any: 'Test'
~$

watashiwa_daredeska ★★★★
()
Ответ на: комментарий от watashiwa_daredeska

Ну, в общем, ключевой момент здесь - if isinstance... Собственно, эту задачу я и решал. Разница между подходом, основанным на existentials и питоновым кодом - тип сообщения, принимаемого хендлером, определяется по хендлеру автоматически, системой типов. Всё остальное, собственно, никак на типизацию не завязано. Разве что над функцией put можно обвязочку сделать: put' msg = put $ Message msg; тогда put' становится полиморфной функцией, посылающей любые сообщения.

Miguel ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

>Обозначением типа может быть что угодно, как удобнее реализовывать

Так вот там, где оно реализуется и описывается операция "сравнение типов". Например "плоские" типы сообщений удобно описывать каким-либо int-ом, "фильтр" будет ф-ей поиска в хеше подписчиков. Иерархические типы удобно описывать "путём", и "фильтр" будет гулять по "дереву" подписчиков, выбирая в т.ч. подписаных на "родственников". К "диспетчеру" это не имеет никакого отношения, однако компилятор проверит совместимость переданых ему компонент. А описания типов самих сообщений равно придётся делать, т.к. GIGO. Другой вопрос - как избавиться от boilerplate-кода при описания оператора сравнения типов. Но на то этот вопрос и "другой", чтобы сильно не углубляться. Можно использовать "компилерскую" магию типа Typeable, или "нагенерировать" код чем-то от вимовских макросов до TemplateHaskellя.

DonkeyHot ★★★★★
()
Ответ на: комментарий от watashiwa_daredeska

Нашёл, наконец

>пример реализации

-- утилиты
processChan handler = do
 q <- newChan
 forkIO (sequence_ . repeat (readChan q >>= handler))
 return (readChan q)

-- диспетчер
dispatch subs msg = subs msg >>= mapM_ send_to
 where send_to handler = handler msg
dispatcher subs = processChan $ dispatch subs

>пример использования
-- сообщения, тип сообщения == номер конструктора
data Message = MsgA Something | MsgB Otherthing | ... 
 deriving (Typeable,Data)

msgtype :: Message -> Int
msgtype = constrIndex . toConstr
msgtypecmp a b = msgtype a == msgtype b

-- подписка - простой хеш со списками, [] если ключа нет
subscribe subs msg handler = insert subs msg (
 handler:lookup' subs msg )

lookup' h k = case lookup h k of 
 Just a -> a
 Nothing -> []

-- подписываем
main = do
 subs <- Data.HashTable.new msgtypecmp msgtype
 subscribe subs (A xxx) handler1
 ...
-- запускаем
 disp <- dispatcher $ lookup' subs
-- посылаем
 dispatcher $ MsgA данные
 ...

DonkeyHot ★★★★★
()
Ответ на: Нашёл, наконец от DonkeyHot

PS: Нашёл опечаток

-  return (readChan q)
+  return (writeChan q)

- dispatcher $ MsgA данные
+ disp $ MsgA данные

DonkeyHot ★★★★★
()

Вы немного перепутали.. В питоне типизация __строгая__, т.е. никаких неявных преобразований не производится, т.е. 1 + "2" # -> Исключение 123 == "123" # -> False при этом __динамическая__, в отличие от статической в джаве, си, плюсах.. Динимическая типизация больше подходит для скриптовых языков, позволяя создавать очень гибкий код. Статическая типизация полезна в основном только для компилируемых языков.

xonix
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.