Data.Typeable.cast в экзистенциальный тип

Итак, это продолжение моей саги о системе объектов (часть 1, часть 2).

Эта часть по существу сводится к следующему.

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE KindSignatures #-}

import Data.Typeable
import GHC.Exts (Constraint)

data Obj (cls :: * -> Constraint) = forall o. (cls o, Typeable o) => Obj o

downcast :: Obj a -> Maybe (Obj b)
downcast (Obj x) = do
                     cx <- cast x
                     return $ Obj cx

Определение downcast завершается с ошибкой:

• Could not deduce: b o0 arising from a use of ‘Obj’
  from the context: (a o, Typeable o)
    bound by a pattern with constructor:
               Obj :: forall (cls :: * -> Constraint) o.
                      (cls o, Typeable o) =>
                      o -> Obj cls,
             in an equation for ‘downcast’
    at downcast.hs:12:11-15
• In the second argument of ‘($)’, namely ‘Obj cx’
  In a stmt of a 'do' block: return $ Obj cx
  In the expression:
    do cx <- cast x
       return $ Obj cx
• Relevant bindings include
    cx :: o0 (bound at moo.hs:13:22)
    downcast :: Obj a -> Maybe (Obj b) (bound at downcast.hs:12:1)

Я не совсем понимаю, почему эта ошибка возникает :( Можно ли это исправить?


person n. 1.8e9-where's-my-share m.    schedule 02.07.2020    source источник
comment
Вы не установили никаких отношений между a и b. Итак, GHC не знает, что одно подразумевает другое.   -  person dfeuer    schedule 02.07.2020
comment
Более конкретно, GHC не знает, как получить b o словарь из a o словаря.   -  person dfeuer    schedule 02.07.2020
comment
И я думаю, что вы довольно обречены, если вы как-то вручную не овеществляете иерархию классов.   -  person dfeuer    schedule 02.07.2020
comment
@dfeuer Более конкретно, GHC не знает, как получить словарь b o из словаря a o Да, это то, что я понял. И я думаю, что вы довольно обречены, если вы каким-то образом вручную не материализуете иерархию классов Я не совсем против этого. Знаете ли вы, как построить такую ​​овеществленную иерархию?   -  person n. 1.8e9-where's-my-share m.    schedule 02.07.2020
comment
В пакете constraints есть некоторая ерунда, которая может вас вдохновить, но я не думаю, что это совсем правильная форма для того, что вы пытаетесь сделать.   -  person dfeuer    schedule 02.07.2020
comment
Ой, подождите.... Вы пытаетесь унизить. Там в принципе нет никакой надежды, не так ли?   -  person dfeuer    schedule 02.07.2020
comment
@dfeuer ну, приведение вверх не проблема, потому что компилятор может получить словарь в направлении вверх ...   -  person n. 1.8e9-where's-my-share m.    schedule 02.07.2020
comment
Даже upcast не будет работать с этим типом! Вам нужно будет добавить (количественное) ограничение, после чего вы можете удалить Maybe. В Haskell нет способа получить коллекцию всех классов, экземпляры которых создаются типом, или проверить, есть ли конкретный класс в этом списке.   -  person dfeuer    schedule 02.07.2020
comment
@dfeuer, конечно, обходится без Maybe (восходящее преобразование всегда должно быть успешным). Я разместил его здесь.   -  person n. 1.8e9-where's-my-share m.    schedule 03.07.2020
comment
Конечно, но это использует дополнительное ограничение, как я указал.   -  person dfeuer    schedule 03.07.2020
comment
@dfeuer ах да, я понимаю, что ты имеешь в виду. Во всяком случае, я пытаюсь решить эту проблему, создав вручную коллекцию экземпляров для каждого типа. Все равно не повезло...   -  person n. 1.8e9-where's-my-share m.    schedule 03.07.2020


Ответы (1)


К тому времени, как ваш Haskell будет переведен в GHC Core, классы (и сопутствующие конструкции логического программирования, такие как имплициация) исчезнут. Они преобразуются компилятором в код передачи словаря — каждый instance становится записью (обычным значением), а каждый метод становится членом этой записи. (Для более подробной информации см. мой предыдущий ответ.)

Таким образом, конструктор, который упаковывает ограничение,

data Obj c where  -- I'm using GADT syntax
    Obj :: c a => a -> Obj c

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

data Obj c where
    Obj :: c a -> a -> Obj c

где поле c a — это словарь методов времени выполнения, представляющий экземпляр c a.

Чтобы преобразовать Obj c в Obj c' во время выполнения, даже если бы у вас был способ проверить, что конкретное a является экземпляром как c, так и c', вам все равно нужно каким-то образом синтезировать словарь для c'. Поскольку c' обычно содержит больше методов, чем c, это равносильно тому, чтобы попросить компьютер написать для вас программу.


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

oracle :: MonadRuntime m => TypeRep a -> TypeRep c -> m (Maybe (Dict (c a)))

тогда вы можете написать cast (с некоторыми неудобными спорами типа):

data Obj c where
    Obj :: c a => TypeRep a -> a -> Obj c

cast :: forall c c' m. (MonadRuntime m, Typeable c') => Obj c -> m (Maybe (Obj c'))
cast (Obj tr x) = do
    mdict <- oracle tr (typeRep @c')
    case mdict of
        Just Dict -> return (Just (Obj tr x))
        Nothing -> return Nothing

Обратите внимание, что этот cast фактически позволяет вам (попытаться) изменить интерфейс вашего объекта на любой другой интерфейс, а не только те, которые являются производными от статического типа объекта. (В C# вы можете сделать это путем повышения до object, а затем понижения.) Вы можете предотвратить это, потребовав entailment в контексте cast:

cast :: forall c c' m. (MonadRuntime m, Typeable c', Class c c') => Obj c -> m (Maybe (Obj c'))

(Конечно, это следствие не будет использоваться во время выполнения.)


Задача состоит в том, чтобы реализовать oracle! Я думаю, что это будет одна из тех задач, которые неинтересны, поэтому я просто дам вам пару советов.

Ваша монада Runtime, вероятно, будет чем-то вроде Reader с таблицей поиска, отображающей (TypeReps) as и cs в словари. as и cs должны быть экзистенциально количественно определены, чтобы хранить их в неоднородном списке.

data TableEntry where
    TableEntry :: c a => TypeRep c -> TypeRep a -> TableEntry

type MonadRuntime = MonadReader [TableEntry]

Затем oracle нужно будет найти TableEntry, соответствующий паре класс/тип, затем открыть экзистенциал, установить равенство типов, разобрав typeRep, и вернуть Just Dict. (В частности, эта часть звучит как кошмар для написания кода.)

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

table = [
    TableEntry (typeRep @Ord) (typeRep @Int),
    TableEntry (typeRep @Eq) (typeRep @Bool)
    ]

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

person Benjamin Hodgson♦    schedule 02.07.2020
comment
Я в порядке с предоставлением словаря вручную. Скажем, каждый тип, который нужно преобразовать, должен быть экземпляром class Castable с методом, предоставляющим список словарей в той или иной форме. Затем downcast находит и возвращает Just правильный словарь или Nothing, если совпадений нет. Я изо всех сил пытаюсь придать этому списку подходящий тип. - person n. 1.8e9-where's-my-share m.; 03.07.2020
comment
Вау, это какое-то продвинутое колдовство, не уверен, что справлюсь... - person n. 1.8e9-where's-my-share m.; 03.07.2020