Функция высшего порядка haskell, которая принимает «шаблон» - PullRequest
0 голосов
/ 16 сентября 2018

Это приводит от мой последний вопрос

Итак, у меня есть

> module HanoiDisk(HanoiDisk, hanoiDisk) where
> data HanoiDisk = HanoiDisk (Maybe Integer) deriving (Show)
> hanoiDisk :: Integer -> HanoiDisk
> hanoiDisk n 
>   | n > 0 = HanoiDisk (Just n)
>   | otherwise = HanoiDisk Nothing

Я написал applyMaybe и инфиксный оператор для работы с этим типом:

> applyMaybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
> applyMaybe f (Just a) (Just b) = Just (f a b)
> applyMaybe _ _ _ = Nothing
>
> infix 5 >>>=
> (>>>=) = applyMaybe

Я хотел оставить applyMaybe и (и инфикс) генерал, поскольку это довольно удобно само по себе.

Но когда я пытаюсь использовать applyMaybe с HanoiDisks, я получаю:

> a = hanoiDisk 5
> b = hanoiDisk 7
> applyMaybe (>) a b

* Couldn't match expected type `Maybe ()'
              with actual type `HanoiDisk'
* In the third argument of `applyMaybe', namely `b'
  In the expression: applyMaybe (>) a b
  In an equation for `it': it = applyMaybe (>) a b

но HanoiDisk это просто псевдоним Maybe Integer, так что это должно работать?!


В конце концов, я понял, что ключевым словом было «псевдоним», поэтому ... вместо использования данных, которые я использовал, я подумал, что смогу ответить на свой вопрос ...

поэтому мой модуль становится

> module HanoiDisk(HanoiDisk, hanoiDisk) where
> type HanoiDisk = Maybe Integer
> hanoiDisk :: Integer -> HanoiDisk
> hanoiDisk n 
>   | n > 0 = (Just n)
>   | otherwise = Nothing

Тогда я могу использовать общую форму моей функции applyMaybe:

> let a = hanoiDisk 4
> let b = hanoiDisk 5
> ((>) >>>= a) b 
Just False

Мне это не нравится, так как вы могли бы

> let t = Just (-4)
> expectsGreaterThanZero :: HanoiDisk -> Bool

Предложения? Я предполагаю, что мне, возможно, придется взглянуть на классы типов?

1 Ответ

0 голосов
/ 16 сентября 2018

Как вы правильно определили, причина исходного сообщения об ошибке заключается в том, что:

data HanoiDisk = HanoiDisk (Maybe Integer)

вводит "алгебраический тип данных", а не "псевдоним типа", поэтому значения aи b имеют тип HanoiDisk, а не Maybe Integer В этом случае возникает ошибка типа, поскольку тип HanoiDisk не имеет формы Maybe a и / или Maybe b, требуемой applyMaybe.

Один из способов выполнить то, что вы хотите - заставить общий applyMaybe работать с типами, которые не Maybe a, но каким-то образом «похожи» на шаблон Maybe a - это ввести класс типов, но перефразировать старыйшутка насчет регулярных выражений: «У нового программиста на Haskell есть проблема, и он решает использовать класс типов. Теперь у программиста есть две проблемы».Ха-ха!

Просто преобразуйте его

Ниже я включил решение класса типов, но более простой и идиоматический способ обработки HanoiDisk как Maybe Integer - обеспечить преобразованиефункция, либо явно, либо путем введения именованного поля в тип данных, как показано ниже.Этот подход используется во всей стандартной библиотеке и в реальном коде на Haskell.

module HanoiDiskConvert where

data HanoiDisk = HanoiDisk { getHanoiDisk :: Maybe Integer }
  deriving (Show)
hanoiDisk :: Integer -> HanoiDisk
hanoiDisk n
  | n > 0 = HanoiDisk (Just n)
  | otherwise = HanoiDisk Nothing

applyMaybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
applyMaybe f (Just a) (Just b) = Just (f a b)
applyMaybe _ _ _ = Nothing
-- or as noted below, just: applyMaybe = liftA2

main = do
  let a = hanoiDisk 5
      b = hanoiDisk 7
  print $ applyMaybe (>) (getHanoiDisk a) (getHanoiDisk b)

Вы должны думать о getHanoiDisk как о моральном эквиваленте необходимости использовать fromIntegral при написании mean xs = sum xs / fromIntegral (length xs),Это просто удовлетворяет требованию Haskell о том, что даже «очевидные» преобразования типов должны быть явными.

Еще одно преимущество этого подхода состоит в том, что - как указано в комментарии - Maybe уже имеет экземпляр Applicative, которыйВы можете использовать здесь.Ваш applyMaybe - это просто специализация liftA2 из Control.Applicative:

import Control.Applicative
applyMaybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
applyMaybe = liftA2

, и в этом модуле есть куча других полезных вещей, которые вы можете использовать.Например, следующее эквивалентно, и синтаксис аппликативного оператора становится легче читать и писать, когда вы приобретаете опыт его использования:

applyMaybe (>) (getHanoiDisk a) (getHanoiDisk b)
(>) <$> getHanoiDisk a <*> getHanoiDisk b      -- using applicative operators

Опрометчивое решение класса типов

В любом случае, еслиВы действительно хотели сделать это, используя класс типов, это выглядело бы примерно так.Вы бы определили класс, который допускает преобразование типа «возможно-подобного» в и из фактического значения «возможно»:

class MaybeLike m a | m -> a where
  toMaybe :: m -> Maybe a
  fromMaybe :: Maybe a -> m

Вы также хотели бы определить экземпляр, разрешающий простые значения Maybeсами по себе должны рассматриваться как возможно!

instance MaybeLike (Maybe a) a where
  toMaybe = id
  fromMaybe = id

Затем вы можете определить экземпляр для вашего HanoiDisk типа:

data HanoiDisk = HanoiDisk (Maybe Integer) deriving (Show)
instance MaybeLike HanoiDisk Integer where
  toMaybe (HanoiDisk x) = x
  fromMaybe x = HanoiDisk x

Наконец, вы можете определить общий applyMaybe которые могут работать с любыми MaybeLike типами путем преобразования в Maybe:

applyMaybe :: (MaybeLike m a, MaybeLike n b, MaybeLike k c)
           => (a -> b -> c) -> m -> n -> k
applyMaybe f m n = fromMaybe $ f <$> toMaybe m <*> toMaybe n

Наконец, это позволит вам написать полную программу:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}

module HanoiClass where

class MaybeLike m a | m -> a where
  toMaybe :: m -> Maybe a
  fromMaybe :: Maybe a -> m

instance MaybeLike (Maybe a) a where
  toMaybe = id
  fromMaybe = id

data HanoiDisk = HanoiDisk (Maybe Integer) deriving (Show)
instance MaybeLike HanoiDisk Integer where
  toMaybe (HanoiDisk x) = x
  fromMaybe x = HanoiDisk x

applyMaybe :: (MaybeLike m a, MaybeLike n b, MaybeLike k c)
           => (a -> b -> c) -> m -> n -> k
applyMaybe f m n = fromMaybe $ f <$> toMaybe m <*> toMaybe n

hanoiDisk :: Integer -> HanoiDisk
hanoiDisk n
  | n > 0 = HanoiDisk (Just n)
  | otherwise = HanoiDisk Nothing

main = do
  let a = hanoiDisk 5
      b = hanoiDisk 7
      res = applyMaybe (>) a b :: Maybe Bool
  print res

Но вы не должны этого делать ...

Проработайте и поиграйте с приведенным выше кодом, если хотите, но поймите, что это ужасная идея для реального дизайна.Одна из причин в том, что сейчас я вижу, что ваш выбор представления для HanoiDisk не очень удачный.Это может начать становиться очевидным для вас, когда вы переключаетесь с написания универсальных функций и классов типов и пытаетесь решить вашу реальную проблему программирования.Например, вы можете написать:

data HanoiTower = HanoiTower [HanoiDisk]

и начать спрашивать себя, что представляет собой это значение:

HanoiTower [HanoiDisk (Just 3), HanoiDisk Nothing]

и почему вы пишете код для его обработки.Тогда вы начнете задаваться вопросом, почему вы когда-либо пытались сравнить HanoiDisk (Just 3) с HanoiDisk Nothing?Когда это когда-нибудь будет полезно?

Наконец, вы поймете, что вы действительно хотите проверять и действовать на допустимых дисковых размерах в start вашей программы, но работать внутренне с представлениемтолько допустимых дисков:

newtype HanoiDisk' = HanoiDisk' Integer

, которые были созданы альтернативным интеллектуальным конструктором:

hanoiDisk' :: Integer -> Maybe HanoiDisk'
hanoiDisk' n | n > 0 = Just (HanoiDisk' n)
             | otherwise = Nothing

или другим кодом, который "очевидно" создает допустимые диски.

В этот момент вы также поймете, что все время, потраченное на написание классов, экземпляров и универсальных функций applyMaybe, было потрачено впустую.

Если вы застряли с более гибким дизайном с паройиз getHanoiDisk вызовов, разбросанных по кругу, у вас будет гораздо меньше бесполезного кода, от которого нужно отказаться.

Если вы исходите из Java-фона или чего-то подобного, вы, вероятно, привыкли к идее разработки сложного шаблона, иерархий объектов и лучших шаблонов проектирования во внешнем интерфейсе, прежде чем писать первую строку полезного кода.,Это может быть эффективным подходом во вселенной Java, но он менее эффективен при программировании на Haskell, особенно когда вы только начинаете.

Хотя это будет трудно, попробуйте сознательно написать наиболее простую вещь.Вы можете написать код, и вам не придется искать искусственные возможности для улавливания неположительных чисел во время компиляции, написания универсальных функций высшего порядка и введения классов типов для решения каждой проблемы.Эти возможности будут развиваться естественным образом, как это редко случается при написании на других языках программирования.Это совет, который мне хотелось бы, чтобы кто-то врезался мне в голову пять лет назад.

...