Я хочу вычислить «арность» определенного класса типов данных . А именно типы данных с одним конструктором и некоторым количеством полей. Например. data T a = T Int () String a . «Арность» будет тогда количеством полей. Для T a это будет 4. Я предполагаю функцию с подписью что-то вроде этого:

forall a . C a => Int

Для некоторого подходящего выбора C. Я знаю, что если у меня есть Generic a для некоторого типа a, я получаю from :: a -> Rep a x, но учтите, что для этого потребуется конкретное значение для a, и мне интересно вычислить его статически. Это возможно как-то? Я также думал о Typeable, но я не совсем понимаю API.

1
fredefox 28 Май 2019 в 23:26

2 ответа

Лучший ответ

Мы можем использовать дженерики. В этом ответе используется несколько расширений, общих для этого разнообразия метапрограммирования. Я упомяну первый раз, когда они используются, но для более подробной информации обратитесь к другим ресурсам, таким как руководство пользователя GHC (список расширений) или Haskell вики.

data T = T Int Bool String deriving Generic

-- Used extension: DeriveGeneric

Производный экземпляр включает экземпляр семейства типов для Rep, который создает общее представление типа T. Rep T использует фиксированный набор типов, найденных в GHC.Generics модуль:

type Rep T = M1 D _ ((M1 C _ (K1 _ Int) :*: M1 C _ (K1 _ Bool)) :*: M1 C _ (K1 _ String))
--
-- Irrelevant details hidden in underscores.
-- There's actually a few more M1's as well
--
-- You can see the full and real details in a ghci session with this command
--   :kind! Rep T

Функция Arity

Мы определим функцию уровня типа для проверки этой структуры и вычисления количества полей. Это его подпись:

type family Arity (f :: Type -> Type) :: Nat
-- If T is a type with one constructor (C x1 ... xn),
-- Arity (Rep T) is the arity n of that constructor

-- Used extensions: TypeFamilies, DataKinds

Когда дело доходит до общего представления, мы можем притворяться, что TT = (Type->Type) похож на ADT со следующими конструкторами:

-- We can pretend that there is this data type TT
-- such that Arity is a function (TT -> Nat)
data TT
  = M1 Type Meta TT
  | (:+:) TT TT
  | V1
  | (:*:) TT TT
  | U1
  | K1 Type Type

Очень (тоже?) Краткий обзор. M1 содержит такую информацию, как имена типов (включая модуль и пакет), имена конструкторов, использует ли конструктор записи записей, строгость полей ... V1 и (:+:) используются для типов с нулем или многими конструкторами, поэтому они не имеют отношения к нам. U1 представляет нулевые конструкторы, а (:*:) разбивает n-арные конструкторы с представлением половины полей с каждой стороны. K1 отмечает одно поле конструктора.

Мы определяем функцию Arity, давая ей экземпляры семейства типов. Но на самом деле, для первого понимания, не обращайте внимания на ключевые слова type instance и притворяйтесь, что Arity - это функция, определяемая сопоставлением с образцом, как обычно.

Глядя на представление Rep T выше, мы сначала сталкиваемся с узлом M1, который мы игнорируем и рекурсивно вызываем Arity для его содержимого.

type instance Arity (M1 i c f) = Arity f

Затем мы видим (:*:), который разбивает множество полей на две части; мы рекурсивно вычисляем их арности и складываем их.

type instance Arity (f :*: g) = Arity f + Arity g

-- Used extensions: TypeOperators, UndecidableInstances

U1 представляет нулевые конструкторы,

type instance Arity U1 = 0

И K1 является одним полем.

type instance (K1 i a) = 1

Теперь, учитывая универсальный тип T (т.е. с экземпляром Generic), Arity (Rep T) является его арностью как уровень типа Nat. В GHCI мы можем проверить это с

:kind! Arity (Rep T)

Используйте GHC.TypeNats.natVal для преобразования его в значение Natural (например, Integer, но неотрицательное).

-- Calculate the arity of the constructor of a generic type `a`.
-- `a` must have a single constructor.
arity :: forall a. (Generic a, KnownNat (Arity (Rep a))) => Natural
arity = natVal (Proxy @(Arity (Rep a)))

-- Used extensions:
--   ScopedTypeVariables,
--   AllowAmbiguousTypes, TypeApplications,
--   FlexibleContexts

Мы получаем арность любого универсального типа T как значение arity @T, которое можно преобразовать, например, с помощью fromIntegral :: Natural -> Integer.

main = print (arity @T)

Полная версия

7
Li-yao Xia 29 Май 2019 в 00:53

Чтобы ответить на ваш вопрос мне в комментариях, вот пример того, как вы можете найти арность функции.

{-# LANGUAGE ScopedTypeVariables, FlexibleInstances #-}

import Data.Proxy

class Arity a where
    arityP :: Proxy a -> Int

instance {-# OVERLAPPABLE #-} Arity a where
    arityP _ = 0

instance {-# OVERLAPPING #-} Arity b => Arity (a -> b) where
    arityP f = 1 + arityP (Proxy :: Proxy b)

arity :: forall a. Arity a => a -> Int
arity _ = arityP (Proxy :: Proxy a)

Я чувствую, что это довольно очевидно, если вы знакомы с вовлеченными идиомами. Это будет хорошо работать для варианта использования, о котором вы спрашивали, где вы пытаетесь найти арность типа данных / конструктора.

ghci> arity T
4

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

ghci> arity id
<interactive>:2:1: error:
• Overlapping instances for Arity a0 arising from a use of ‘arity’
  Matching instances:
    instance [overlappable] [safe] Arity a -- Defined at arity.hs:10:31
    instance [overlapping] [safe] Arity b => Arity (a -> b)
      -- Defined at arity.hs:13:30

Это имеет смысл, потому что id потенциально имеет несколько арностей, в зависимости от того, где он создан

id :: Int -> Int
id :: (Int -> Int) -> Int -> Int

Что на самом деле увеличивает мою уверенность в этом подходе. Дайте мне знать, как это работает.

5
luqui 28 Май 2019 в 21:54