#haskell #typeclass #applicative
#haskell #класс типов #applicative
Вопрос:
Все еще не на сто процентов уверен, как создавать экземпляры более сложных типов. Имейте это:
data CouldBe a = Is a | Lost deriving (Show, Ord)
Создал экземпляр Functor
, используя Maybe
в качестве примера:
instance Functor CouldBe where
fmap f (Is x) = Is (f x)
fmap f Lost = Lost
Для выполнения чего-то подобного:
tupleCouldBe :: CouldBe a -> CouldBe b -> CouldBe (a,b)
tupleCouldBe x y = (,) <$> x <*> y
CouldBe
должен быть экземпляр Applicative
, но как бы вы это сделали? Конечно, я могу найти это и скопировать, но я хочу изучить процесс, стоящий за этим, и, наконец, получить instance
объявление CouldBe.
Комментарии:
1. Начните с записи функций, которые вам нужно определить, чтобы создать
CouldBe
экземплярApplicative
, с их сигнатурами типов (специализированными дляCouldBe
). Вы должны обнаружить, что на самом деле есть только одно разумное решение, если вы позволите типам направлять вас. И вашFunctor
экземпляр показывает вам, как обращатьсяLost
.2. Максимально сопоставьте шаблон, а затем создайте результаты единственным способом, который проверяет тип. Это часто срабатывает.
3. Логически я бы определял
pure
и<*>
, правильно? Я думаю, что pure должен быть простым, какpure a = Is a
, но не<*>
требует дополнительных вариантов…4. Попробуйте сопоставление шаблонов с каждой стороны
<*>
; это должно дать четыре разных случая. Затем воспользуйтесь советом @RobinZigmond и «следуйте типам» для каждого случая. Еще одна вещь, которая могла бы помочь, — это пробелы в наборе текста: если вы не знаете, что куда поместить, используйте символ подчеркивания_
, и GHC выдаст вам тип, который должен быть там. Они невероятно полезны в такого рода ситуациях «следования типам».5. Обратите внимание, что ваш
CouldBe
изоморфен встроенномуMaybe
типу Haskell, поэтому в любой момент, когда вы захотите узнать, как написать экземпляр для вашего типа, вы можете просто посмотреть, как написан соответствующий экземпляр дляMaybe
.
Ответ №1:
Вы просто записываете это, следуя типам:
instance Applicative CouldBe where
{-
Minimal complete definition:
pure, ((<*>) | liftA2)
pure :: a -> f a
pure :: a -> CouldBe a
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 :: (a -> b -> c) -> CouldBe a -> CouldBe b -> CouldBe c
-}
pure a = fa
where
fa = ....
liftA2 abc fa fb = fc
where
fc = ....
Согласно
data CouldBe a = Is a | Lost
наш набор инструментов
Is :: a -> CouldBe a
Lost :: CouldBe a
но мы также можем использовать сопоставление с шаблоном, например
couldBe is lost (Is a) = is a
couldBe is lost (Lost) = lost
couldBe :: ? -> ? -> CouldBe a -> b
couldBe :: ? -> b -> CouldBe a -> b
couldBe :: (a -> b) -> b -> CouldBe a -> b
Итак,
-- pure :: a -> f a
pure :: a -> CouldBe a
совпадает с
Is :: a -> CouldBe a
итак, мы определяем
pure a = Is a
Затем, для liftA2
, мы следуем за примерами данных:
-- liftA2 :: (a -> b -> c) -> f a -> f b -> f c
-- liftA2 :: (a -> b -> c) -> CouldBe a -> CouldBe b -> CouldBe c
liftA2 abc Lost _ = ...
liftA2 abc _ Lost = ...
liftA2 abc (Is a) (Is b) = fc
where
c = abc a b
fc = .... -- create an `f c` from `c`:
-- do we have a `c -> CouldBe c` ?
-- do we have an `a -> CouldBe a` ? (it's the same type)
Но в первых двух случаях у нас нет a
или a b
; поэтому нам приходится создавать CouldBe c
из ничего. У нас также есть этот инструмент в нашем наборе инструментов.
Выполнив все недостающие части, мы можем подставлять выражения непосредственно в определения, устраняя все ненужные промежуточные значения / переменные.
Ответ №2:
С моим новым идиоматическим пакетом вы можете выводить Applicative
для сумм.
{-# Language DataKinds #-}
{-# Language DeriveGeneric #-}
{-# Language DerivingVia #-}
{-# Language StandaloneKindSignatures #-}
import Data.Kind
import GHC.Generics
import Generic.Applicative
type CouldBe :: Type -> Type
data CouldBe a = Is a | Lost
deriving
stock (Eq, Ord, Show, Generic1)
-- > pure @CouldBe 10
-- Is 10
-- > liftA2 ( ) (Is 1) Lost
-- Lost
-- > liftA2 ( ) Lost (Is 10)
-- Lost
deriving (Functor, Applicative)
via Idiomatically CouldBe '[LeftBias Terminal]
-- > tupleCouldBe Lost Lost
-- Lost
-- > tupleCouldBe (Is 1) Lost
-- Lost
-- > tupleCouldBe Lost (Is 20)
-- Lost
-- > tupleCouldBe (Is 1) (Is 20)
-- Is (1,20)
tupleCouldBe :: CouldBe a -> CouldBe b -> CouldBe (a, b)
tupleCouldBe = liftA2 (,)
Почему это работает? Смещение влево означает, что мы выбираем Is
конструктор в качестве pure
конструктора.
Это означает, что мы «отклоняемся» от этого конструктора при переходе к другим конструкторам.
Terminal
описывает, как мы преобразуем любой Applicative
в Const mempty
data Terminal
instance (Applicative f, Monoid m) => Idiom Terminal f (Const m) where
idiom :: f ~> Const m
idiom = mempty
который в этом случае отбрасывает аргумент Is
сопоставления с Lost
.
Обратите внимание, что нет способа определить правостороннее определение CouldBe
, поскольку для этого потребовался бы аппликативный морфизм, который создает a
из ничего
via Idiomatically CouldBe '[RightBias ..]
idiom :: Const () ~> Identity
idiom (Const ()) = Identity ??