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