Основы классов классов Haskell и "не удалось вывести (~) из контекстной (~)" ошибки - программирование

Основы классов классов Haskell и "не удалось вывести (~) из контекстной (~)" ошибки

Я относительно новичок в Haskell, и я считаю, что я неправильно понимаю что-то фундаментальное в классах классов. Предположим, я хотел бы создать класс типа 'T', реализующий n-арные деревья, поддерживаемые четырьмя алгебраическими типами A, B, C и D, структура которых налагает максимальную глубину в четыре. Это кажется глупым примером, но я думаю, что это лучше всего иллюстрирует мою мысль.

module Test where

class T t0 where
    parent :: T t1 => t0 -> Maybe t1
    children :: T t1 => t0 -> [t1]

data A = A [B]
instance T A where
    parent (A _) = Nothing
    children (A bs) = bs

data B = B A [C]
instance T B where
    parent (B a _) = Just a
    children (B _ cs) = cs

data C = C B [D]
instance T C where
    parent (C b _) = Just b
    children (C _ ds) = ds

data D = D C
instance T D where
    parent (D c) = Just c
    children (D _) = []

Я бы хотел написать общие родительские и дочерние функции, но GHC не имеет ни одного из них.

Test.hs:10:27:
    Could not deduce (t1 ~ B)
    from the context (T t1)
      bound by the type signature for children :: T t1 => A -> [t1]
      at Test.hs:10:9-28
      `t1' is a rigid type variable bound by
           the type signature for children :: T t1 => A -> [t1]
           at Test.hs:10:9
    Expected type: [t1]
      Actual type: [B]
    In the expression: bs
    In an equation for `children': children (A bs) = bs
    In the instance declaration for `T A'

Test.hs:14:31:
    Could not deduce (t1 ~ A)
    from the context (T t1)
      bound by the type signature for parent :: T t1 => B -> Maybe t1
      at Test.hs:14:9-31
      `t1' is a rigid type variable bound by
           the type signature for parent :: T t1 => B -> Maybe t1
           at Test.hs:14:9
    In the first argument of `Just', namely `a'
    In the expression: Just a
    In an equation for `parent': parent (B a _) = Just a

Test.hs:15:29:
    Could not deduce (t1 ~ C)
    from the context (T t1)
      bound by the type signature for children :: T t1 => B -> [t1]
      at Test.hs:15:9-30
      `t1' is a rigid type variable bound by
           the type signature for children :: T t1 => B -> [t1]
           at Test.hs:15:9
    Expected type: [t1]
      Actual type: [C]
    In the expression: cs
    In an equation for `children': children (B _ cs) = cs
    In the instance declaration for `T B'

Test.hs:19:31:
    Could not deduce (t1 ~ B)
    from the context (T t1)
      bound by the type signature for parent :: T t1 => C -> Maybe t1
      at Test.hs:19:9-31
      `t1' is a rigid type variable bound by
           the type signature for parent :: T t1 => C -> Maybe t1
           at Test.hs:19:9
    In the first argument of `Just', namely `b'
    In the expression: Just b
    In an equation for `parent': parent (C b _) = Just bv

Я (думаю, я) понимаю, что классы классов совсем не похожи на интерфейсы Java, поскольку функции уровня класса должны работать для любого возможного значения предоставленных переменных типа; вызывающий абонент не "решает" тип. Я не понимаю, почему GHC не может вывести (t1 ~ _), потому что тип, замененный на t1, всегда является экземпляром "T". Я вижу, что существует некоторая циклическая зависимость между объявлениями экземпляра, например. Объявление экземпляра зависит от того, насколько B действителен, что зависит от A и C и т.д., Но я чувствую, что GHC достаточно умен, чтобы понять это, и я просто что-то пропустил. Я всегда получаю эту ошибку всякий раз, когда мне хочется, чтобы функция класса типа принимала один тип в классе, но возвращала другую... Есть ли способ выполнить это с помощью классов классов?

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

Спасибо заранее.

4b9b3361

Ответ 1

Вы правильно поняли проблему: эти подписи действительно означают

parent :: forall t1 . T t1 => t0 -> Maybe t1

скорее то, что у вас было бы на ковариантном языке OO,

parent :: exists t1 . T t1 => t0 -> Maybe t1

Два обычных решения для такого рода вещей - использовать аналогичные синтаксические расширения

  • TypeFamilies

    class T t0 where
      type Child t0 :: *
      parent :: Child t0 -> Maybe t0
      children :: t0 -> [Child t 0]
    
    instance T A where
      type Child A = B
      parent (A _) = Nothing
    
    ...
    
  • или MultiparamTypeClasses

    class T child t0 where
      parent :: child -> Maybe t0
      children :: t0 -> [child]
    
    instance T A B where
    ...
    

Обратите внимание, что в обоих случаях D не будет иметь экземпляр.

Что касается того, какое из этих расширений "лучше" - вы не можете на это ответить. MultiparamTypeClasses сам по себе часто слишком "слаб" в том, что вам нужно исправить все задействованные типы вручную, но это можно снять, добавив FunctionalDependency. В частном случае class T child t0 | t0 -> child, что в значительной степени эквивалентно решению TypeFamilies. Но в вашем случае возможно class T child t0 | t0 -> child, child -> t0.

Подробнее см. Haskell Wiki.

Ответ 2

Это действительно не ответ на ваш конкретный вопрос, но это решение вашей проблемы: создайте структуру дерева k-ary, максимальная глубина которой ограничена его типом. Если вы не заинтересованы в использовании большого количества расширений GHC, это решение довольно простое и расширяемое.

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

{-# LANGUAGE 
    MultiParamTypeClasses
  , DataKinds
  , GADTs
  , FunctionalDependencies
  , KindSignatures
  , FlexibleInstances
  , UndecidableInstances
  , PolyKinds
  , TypeOperators
  , FlexibleContexts
  , TypeFamilies
  #-} 
  -- Not all of these are needed; most are used to make the code cleaner

data Nat = Z | P Nat

Тип Nat используется для кодирования глубины на уровне типа. Используя -XDataKinds, GHC может взять простой тип данных, например Nat и "поднять" его; то есть конструкторы данных становятся типами, а тип Nat становится "видом" (тип типа). Z == ноль; P == плюс один.

type family LTEQ (a :: Nat) (b :: Nat) :: Bool
type instance LTEQ Z Z = True
type instance LTEQ Z (P x) = True
type instance LTEQ (P x) Z = False
type instance LTEQ (P a) (P b) = LTEQ a b

Далее определим частичный порядок на Nat. Обратите внимание на явные сигнатуры вида (т.е. - a :: Nat) - они не нужны с PolyKinds, но уточняют, что происходит. Если это выглядит запутанным, просто подумайте об этом как о наборе правил:

0 <= 0 == True
0 <= (1 + x) == True
(1 + x) <= 0 == False
(1 + x) <= (1 + y) == x <= y

Если вы хотите доказать себе, что это работает:

-- This would only be used for testing
data Pr p = Pr

lteq :: f ~ (a `LTEQ` b) => Pr a -> Pr b -> Pr f
lteq _ _ = Pr

>:t lteq (Pr :: Pr (P Z)) (Pr :: Pr Z)
lteq (Pr :: Pr (P Z)) (Pr :: Pr Z) :: Pr Bool 'False
>:t lteq (Pr :: Pr (P Z)) (Pr :: Pr (P Z))
lteq (Pr :: Pr (P Z)) (Pr :: Pr (P Z)) :: Pr Bool 'True
>:t lteq (Pr :: Pr (P Z)) (Pr :: Pr (P (P Z)))
lteq (Pr :: Pr (P Z)) (Pr :: Pr (P (P Z))) :: Pr Bool 'True
>:t lteq (Pr :: Pr Z) (Pr :: Pr (P (P Z)))
lteq (Pr :: Pr Z) (Pr :: Pr (P (P Z))) :: Pr Bool 'True
>:t lteq (Pr :: Pr Z) (Pr :: Pr Z)
lteq (Pr :: Pr Z) (Pr :: Pr Z) :: Pr Bool 'True

Мы должны использовать Pr, а не только a -> b -> (LTEQ a b), потому что a и b являются поднятыми типами (в частности, вида Nat), а (->) используются только непереходные типы. Если это не имеет смысла, не беспокойтесь слишком много, так как это не имеет большого значения. Достаточно сказать, что это необходимо здесь.

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

type MAXDEPTH = P (P (P (P Z)))

Обратите внимание, насколько просто изменить максимальную глубину. Теперь тип данных Tree. Он использует синтаксис GADT (обобщенный алгебраический тип данных); в основном все это означает, что мы получаем больше контроля над тем, как вы можете создать нечто вроде типа Tree. Переменная типа d - это глубина, a - это элемент, хранящийся в дереве.

data Tree d a where
    D0    :: a -> Tree Z a     
    DPlus :: ((P d) `LTEQ` MAXDEPTH) ~ True => a -> [Tree d a] -> Tree (P d) a 

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

DPlus принимает node и список поддеревьев. Добавление одного node, очевидно, увеличивает глубину на единицу - вы можете видеть, что тип результата отражает это. Затем, чтобы обеспечить максимальную глубину 4, мы просто скажем, что d + 1 <= MAXDEPTH.

Поскольку 0 деревьев глубины довольно скучно, вам, вероятно, понадобится вспомогательная функция для глубины 1:

depth1 a xs = DPlus a (map D0 xs)

Затем экземпляр show просто для удовольствия:

instance Show a => Show (Tree d a) where
    show (D0 a)     = "D0 " ++ show a
    show (DPlus a xs) = "DPlus " ++ show a ++ " " ++ show xs

И быстрый тест:

>depth1 'c' "hello"
DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']

>DPlus 'a' [depth1 'c' "hello"]
DPlus 'a' [DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']]

>DPlus 'b' [DPlus 'a' [depth1 'c' "hello"]]
DPlus 'b' [DPlus 'a' [DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']]]

>DPlus 'c' [DPlus 'b' [DPlus 'a' [depth1 'c' "hello"]]]
DPlus 'c' [DPlus 'b' [DPlus 'a' [DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']]]]

>DPlus 'd' [DPlus 'c' [DPlus 'b' [DPlus 'a' [depth1 'c' "hello"]]]]
<interactive>:130:1:
    Couldn't match type 'False with 'True
    Expected type: 'True ...

Как вы можете видеть, попытка построить дерево с глубиной более 4 приведет к ошибке типа.

Быстрое примечание: ваш примерный код предназначен для деревьев, которые позволяют ссылаться на их структуру. Поскольку основной целью моего ответа было продемонстрировать использование DataKinds для обеспечения глубины дерева на уровне типа, я просто реализовал очень простое дерево. У вас есть правильная идея для ссылки на "вверх" дерево, но так как теперь все это один тип, вам, вероятно, даже не нужны стили класса!