Я хочу рассчитать «арность» определенного класса типы данных. А именно типы данных с одним конструктором и некоторым количеством полей. Например. 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.
Я не уверен, что следую. Как бы вы реализовали forall a . Generic a => Int
?
Если вы просто пытаетесь избежать передачи конкретного значения для a
, вы можете использовать значение Proxy
или расширение TypeApplications
. Если вы хотите, чтобы эти вычисления происходили во время компиляции, а не во время выполнения, я не удивлюсь, если GHC сделает это независимо во время оптимизации, но я не знаю наверняка.
Проблема не в звонилке. Проблема заключается в полной реализации функции с этой сигнатурой. Если я использую from
для реализации этой функции, это абсолютно должен проверяет значение. Возьмем, к примеру, from @(T ()) undefined
, который находится внизу.
Найти арность конструктора довольно просто.. например. называя это arity T
(T
на уровне значений)
@luqui как ты реализуешь arity
? Или, если вы имеете в виду библиотечную функцию, можете ли вы предоставить ссылку?
Чтобы ответить мне на ваш вопрос в комментариях, вот пример того, как вы можете найти арность функции.
{-# 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
Что на самом деле увеличивает мою уверенность в этом подходе. Дайте мне знать, как это работает.
Это не совсем то, к чему я стремился. Я могу понять, если не очень ясно, к чему я стремлюсь. Во-первых, я должен был назвать конструктор значений и типов в моем примере по-другому. Ваша реализация arity
все еще нуждается в передаче значения. Я надеялся, что, например. Typeable
позволил бы мне ответить на вопрос, предполагая, что некоторый тип T
принадлежит классу, описанному выше, сколько у него полей. В контексте, в котором я хочу использовать это, мой тип появляется только в положительной позиции. Поэтому я не могу передать его в другую функцию. Используйте только информацию на уровне типа.
@fredefox да, я отвечал на твой вопрос в комментариях. Вы можете получить то, что ищете, используя GHC.Generics
.
Ах да, вы действительно сказали арность конструктор.
У типа может быть много конструкторов, каждый из которых имеет разную арность. GHC не знает, что у вашего типа есть только один конструктор, если только вы не используете Generics
- см. ответ Ли-яо Ся. Поэтому, если вам нужна арность конструктора, вы должны предоставить конструктор, то есть значение, а не только тип данных.
Мы можем использовать дженерики. В этом ответе используется довольно много расширений, общих для этого разнообразия метапрограммирования. Я упомяну первый раз, когда они используются, но для получения более подробной информации обратитесь к другим ресурсам, таким как руководство пользователя 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
Мы определим функцию уровня типа для проверки этой структуры и вычисления количества полей. Это его подпись:
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)
похож на АТД со следующими конструкторами:
-- 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)
Полная суть: https://gist.github.com/Lysxia/10f1da354f051b2d2eb24f6aace1bf9c
Ух ты! Для этого потребовалось гораздо больше техники, чем я думал. Я думал, что ответ будет в Rep
, как указал @Fyodor Soikin в комментариях, но, поскольку это сам по себе тип, я не мог понять, как написать такую функцию. Я полагаю, мне следует изучить «Мышление с типами». Интересно, сделает ли -XDependentTypes
решение более элегантным... Отличный ответ!
Надеюсь, вы не возражаете, что я использовал его здесь: github.com/fredefox/mysql-simple/blob/fredefox/generic-quasi/… для этого PR: github.com/paul-rouse/mysql-simple/pull/50
Конечно, это совершенно бесплатно!
Вы можете использовать
Generic
на уровне типа. Вам не нужно создавать значениеRep
, вы можете проверить его тип.