Создание экземпляра Applicative

#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 ??