В этом случае то, что вы хотите, должно быть возможным, потому что QueryEvent
или UpdateEvent
- это Method
, а Method
- это Typeable
.Typeable
позволяет нам использовать функции из Data.Typeable
для проверки того, какой конкретный тип у нас есть во время выполнения, что мы обычно не можем сделать.
Вот небольшой автономный пример, который напрямую не используетacid-state
, но начинает иллюстрировать идею:
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PatternSynonyms #-}
Это не является строго необходимым, но позволяет сделать более приятный синтаксис для сопоставления на Event
s.
import Data.Typeable
Нам нужны функции из этого модуля для доступа к информации о наборе во время выполнения.
data GetVersion = GetVersion
data GetUser = GetUser String
class Typeable a => QueryEvent a where
instance QueryEvent GetVersion where
instance QueryEvent GetUser where
Упрощенный набор типов / классов для эмуляции того, что должен генерировать acid-state
.
pattern IsEvent p <- (cast -> Just p)
Этот «синоним шаблона» делает так, что мы можем записать IsEvent p
в LHS соответствия шаблона и заставить его работать так же, как если бы мы написали (cast -> Just p)
.Этот последний является «шаблоном представления», который, по сути, запускает функцию cast
на входе, а затем шаблон сопоставляет ее с Just p
.cast
- это функция, определенная в Data.Typeable
: cast :: forall a b. (Typeable a, Typeable b) => a -> Maybe b
.Это означает, что если мы напишем, например, (cast -> Just GetVersion)
, то получится, что cast
попытается преобразовать аргумент в значение типа GetVersion
, которое затем сопоставляется с шаблоном значения GetVersion
уровня значения;если преобразование завершается неудачно (подразумевается, что событие является чем-то другим), cast
возвращает Nothing
, поэтому этот шаблон не совпадает.Это позволяет нам писать:
exampleFunction :: QueryEvent a => a -> String
exampleFunction (IsEvent GetVersion) = "get version"
exampleFunction (IsEvent (GetUser a)) = "get user " ++ a
Это тогда работает:
λ> exampleFunction GetVersion
"get version"
λ> exampleFunction (GetUser "foo")
"get user foo"
Ваша ситуация немного сложнее, поскольку (тип) RHS функции зависитна тип входа.Для этого нам понадобятся дополнительные расширения:
{-# LANGUAGE GADTs #-} -- For type equality
{-# LANGUAGE TypeOperators #-} -- For type equality
{-# LANGUAGE TypeFamilies #-} -- For EventResult
{-# LANGUAGE ScopedTypeVariables #-} -- For writing castWithWitness
{-# LANGUAGE TypeApplications #-} -- For convenience
Мы также можем добавить EventResult
к нашему простому пустышку QueryEvent
:
class Typeable a => QueryEvent a where
type EventResult a
instance QueryEvent GetVersion where
type EventResult GetVersion = Int
instance QueryEvent GetUser where
type EventResult GetUser = String
Вместо использования cast
, мыможно использовать
castWithWitness :: forall b a. (Typeable a, Typeable b)
=> a -> Maybe (b :~: a, b)
castWithWitness x = case eqT @a @b of
Nothing -> Nothing
Just Refl -> Just (Refl, x)
@a
и @b
используют TypeApplications
для применения eqT
к типам, к которым применен castWithWitness
, которые связаны через ScopedTypeVariables
с использованием forall
в подписи типа.castWithWitness
похоже на cast
, но в дополнение к «приведенной» переменной он возвращает доказательство того, что переданные типы одинаковы.К сожалению, это усложняет использование: синоним шаблона IsEvent
не может быть использован, и соответствующий тип должен быть передан напрямую:
exampleFunction :: forall a. QueryEvent a => a -> EventResult a
exampleFunction (castWithWitness @GetVersion -> Just (Refl, GetVersion)) = 1
exampleFunction (castWithWitness @GetUser -> Just (Refl, GetUser n)) = n
Это работает, потому что в каждом случаепосле сопоставления с Refl
GHC знает по RHS функции, что такое a
, и может уменьшить семейство типов EventResult
.