Re[12]: Зачем нужно наследование интерфейсов?
От: Воронков Василий Россия  
Дата: 18.07.12 14:58
Оценка:
Здравствуйте, Klapaucius, Вы писали:

K>Кстати, "наследование" тайпклассов в хаскеле — фича сомнительной полезности, да еще и, в некоторых случаях, достаточно проблемная.

K>Почему сомнительной полезности? И почему проблемная?
K>Члены тайпкласса "наследника" редко могут быть реализованы в терминах базового класса (тут наследование могло бы помочь), но зато очень часто базовый класс может быть реализован в терминах наследника (тут наследование ничем помочь не может). Возможно, полезность наследованию могли бы придать так называемые дефолтные реализации для суперклассов:
K>
K>class Functor f => Applicative f where
K>    return :: x -> f x
K>    (<*>) :: f (s -> t) -> f s -> f t
K>    (>>) :: f s -> f t -> f t
K>    fs >> ft = return (flip const) <*> fs <*> ft
K>    -- описываем дефолтный инстанс для суперкласса.
K>    instance Functor f where
K>        fmap = (<*>) . return
        
K>class Applicative f => Monad f where
K>    (>>=) :: f a -> (a -> f b) -> f b
K>    instance Applicative f where
K>        ff <*> fs = ff >>= \f -> fs >>= \s -> return (f s)
K>


Здесь, собственно, какой момент. Вот "большевики" долго говорят о том, что монада должна быть апликативным функтором. Вернее, даже не так. Монада *является* апликативным функтором. Возникает естественное желание это самое "является" как-то зафиксировать. Наследование — как раз способ подобной фиксации. И теперь, собственно, вопрос. А что мы теряем, если на уровне абстракции подобной фиксации не будет? Т.е. понятно, что сейчас ее и так нет. Но все же — есть ли какие-то *реальные* проблемы из-за этого?

Одна проблема мне понятна (кстати, не понимаю, как тут помогут те же default-ы) — реализуя Монаду, я не могу получить реализацию того же функтора "из коробки", мне придется ее-таки описать (да, я могу описать ее через готовые функции, но это-таки бойлерплейт). Эту проблему могли бы решить как раз "дефолтные реализации для суперклассов". Или даже без них — сама монада (ну при наличии немного другой, более "православной" иерархии классов) могла бы быть описана в терминах map/join — таким образом, я бы реализовал map/join, а реализацию монады получил бы за бесплатно. Без наследования же — только бойлерплейт (?)

А что еще?

K>Наследование, в принципе, позволяет избавиться от огромных контекстов. Но на практике для этого используется редко (городить такую сильную связь ради некоторого уменьшения писанины — спорное решение). Тем более, что в современном хаскеле есть средства куда луше:

K>
K>type NumAndEq a = (Num a, Eq a)
K>foo :: NumAndEq a => a -> a -> Bool
K>foo a b = a * b == a
K>

K>Т.е. в хаскеле сейчас тенденция такая: отказ от наследования в стандартных библиотеках, добавление более адекватных (это, впрочем, еще время покажет — или не покажет) средств решения проблем без всякого наследования.

А нет каких-либо тредов или тикетов на тему "а давайте выпилим наследование"? Было бы интересно почитать.
Re[13]: Зачем нужно наследование интерфейсов?
От: Klapaucius  
Дата: 23.07.12 09:03
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

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


Да, тут фиксация безвредна, потому что достоверно известно, что для всего, для чего можно написать инстанс монады — можно и интсанс аппликативного функтора написать. Это, однако, скорее исключение: в большинстве случаев никакой уверенности в том, что при возможности реализовать класс C можно реализовать и суперкласс S нет. А это порождает проблемы аналогичные всяким интерфейсным методам, выбрасывающим NotImplementedException. К примеру, имплементация Num для функций из чисел в числа вполне имеет смысл и может быть написана, но реализации Show и Eq — нет. "Наследование" вынуждает их писать.

ВВ>И теперь, собственно, вопрос. А что мы теряем, если на уровне абстракции подобной фиксации не будет? Т.е. понятно, что сейчас ее и так нет. Но все же — есть ли какие-то *реальные* проблемы из-за этого?


Проблемы вполне были, но не особенно серьезные. Дело в том, что если что-то можно не написать — его и не напишут. Например, долгое время не было имплементации Applicative для ST. Это вызывало некоторые неудобства. Есть, конечно, обертки, превращающие любой инстанс Monad в инстанс Applicative, но они замусоривают код. Кроме того, теоретически можно реализовать Applicative и Monad по разному — а это уже хитрый баг. Наследование тут, правда, не сильно поможет, разве что тем, что сократит время между реализациями (в нашем случае инстанс Monad писали, допустим, в 1995, a Applicative — в 2005).

Вообще, несуществующая в Prelude иерархия Functor -> Applicative -> Monad сама по себе является демонстрацией проблем "наследования". Суперкласс трудно добавить позднее. Даже если такую иерархию поправить — рано или поздно появится какой-нибудь Pointed (между Functor и Applicative). Особая ирония в том, что иерархия эта фундаментальна, известна была за годы до появления хаскеля и достоверно свободна от проблем типа наследования Num от Eq. Но даже в этих тепличных условиях "наследование" подвело. С подавляющим большинством других иерархий проблем будет только больше.

ВВ>Одна проблема мне понятна (кстати, не понимаю, как тут помогут те же default-ы) — реализуя Монаду, я не могу получить реализацию того же функтора "из коробки", мне придется ее-таки описать (да, я могу описать ее через готовые функции, но это-таки бойлерплейт). Эту проблему могли бы решить как раз "дефолтные реализации для суперклассов". Или даже без них — сама монада (ну при наличии немного другой, более "православной" иерархии классов) могла бы быть описана в терминах map/join — таким образом, я бы реализовал map/join, а реализацию монады получил бы за бесплатно. Без наследования же — только бойлерплейт (?)


Ну так "наследование" тут как раз не особенно помогает, о чем я и писал. А default-реализации помогают. Сравните сами:
-- нет "наследования" и default-методов:
instance Functor F where
    fmap = ...
    
instance Applicative F where
    pure = ...
    (<*>) = ...
    
instance Monad F where
    return = ...
    (>>=) = ...
-- реализуем пять функций.

-- теперь исправленная иерархия с "наследованием":
instance Functor F where
    fmap = ...
    
instance Applicative F where
    pure = ...
    (<*>) = ...
    
instance Monad F where
    join = ...

-- реализуем четыре функции. При этом join не сильно легче bind, так что
-- наследование спасло нас только от повторной реализации pure/return 
-- самой простой из четырех.

-- А теперь варианты с default-ами:

instance Functor F -- эти инстансы не обязательны.

instance Applicative F -- в отличие от случая с "наследованием".

instance Monad F where
    return = ...
    (>>=) = ... -- либо join, зависит от того, как класс определили.
    
-- реализуем две функции.
-- если же у нас уже есть готовые инстансы Functor и Applicative
-- что, само по себе, явление редкое (Functor можно вывести автоматически, Applicative - нет)
-- то все будет как при "идеальной иерархии"
-- return повторно реализовывать не нужно.


ВВ>А нет каких-либо тредов или тикетов на тему "а давайте выпилим наследование"? Было бы интересно почитать.


"Наследование" как фичу? Нет, никто его "выпиливать" не собирается. Все-таки это наследование в кавычках — аналогом наследования является в самом простом случае. На самом деле это контексты, аналог констрейнтов дженериков, в более интересных случаях наследованию не эквивалентны, и претензии к "сомнительной полезности" к контекстам в общем случае не применимы. Например:
class (Monad m, Monad (t m)) => Trans t m where
-- или там
class (FunDep a ~ b) => C a b where type FunDep a

Тут уже с наследованием мало общего.
... << RSDN@Home 1.2.0 alpha 4 rev. 1476>>
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Re[2]: Зачем нужно наследование интерфейсов?
От: Klapaucius  
Дата: 23.07.12 11:11
Оценка:
Здравствуйте, PSV100, Вы писали:

PSV>В этой ветке
Автор: Temoto
Дата: 06.07.12
начали говорить о Go, но не договорили. Интерфейсы у него, фактически, аналог классов типов в Хаскеле или протоколов и типов в Кложуре и т.п.:


PSV>
PSV>type Reader interface {
PSV>    Read(b []byte) (n int, err error)
PSV>}

PSV>type Writer interface {
PSV>    Write(b []byte) (n int, err error)
PSV>}

PSV>type ReadWriter interface {
PSV>    Reader
PSV>    Writer
PSV>}
PSV>


PSV>Фактически, это аналог хаскелевских деклараций вида:


PSV>class (Eq a, Show a) => C a where ...


И близко не аналог. Принципиальное отличие выделено жирным шрифтом. Как, например, на го-интерфейсах делается упомянутый вами класс Eq?
... << RSDN@Home 1.2.0 alpha 4 rev. 1476>>
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.