具有“模式”的haskell高阶函数

时间:2018-09-16 14:19:47

标签: haskell types polymorphism alias

这从my last question

开始

所以我有

> 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和(和infix)一般,因为它本身很方便。

但是,当我尝试将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 :(得分:1)

您已正确确定,原始错误消息的原因是:

data HanoiDisk = HanoiDisk (Maybe Integer)

引入了“代数数据类型”而不是“类型别名”,因此ab的值属于HanoiDisk类型,而不是Maybe Integer类型错误然后产生结果,因为类型HanoiDisk的格式不是Maybe a要求的Maybe b和/或applyMaybe形式。

一种完成您想要的方式的方法-引入一般的applyMaybe来处理不是Maybe a的类型,但以某种方式“看起来”像模式Maybe a的方式是-类型类,但是用一个正则表达式的老话来解释:“一个新的Haskell程序员遇到了问题,决定使用类型类。现在,程序员遇到了两个问题。”哈哈!

只需将其转换

我在下面包括了一个类型类解决方案,但是将HanoiDiskMaybe 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只是liftA2Control.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进行比较?什么时候有用?

最后,您将意识到,您确实希望在程序的开始处检查有效磁盘大小并对其进行操作,但在内部只能使用有效磁盘的表示形式进行操作:

newtype HanoiDisk' = HanoiDisk' Integer

已由备用智能构造函数创建的

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

或“显然”创建有效磁盘的其他代码。

在这一点上,您还将意识到花在编写类型类,实例和泛型applyMaybe函数上的所有时间都是浪费的。

如果您坚持使用更加敏捷的设计,并伴随着两​​个getHanoiDisk调用,那么您将放弃很多无用的代码。

如果您来自Java或其他背景,您可能已经习惯了在编写第一行有用代码之前在前端开发精致的样板,对象层次结构和最佳实践设计模式的想法。 。在Java世界中,这可能是一种有效的方法,但是在对Haskell进行编程时效果不佳,尤其是在您刚入门时。

尽管这很困难,但请尝试着有意识地编写尽可能简单的代码,不要为寻找编译时捕获非正数的人为机会而努力,编写通用的高阶函数,并介绍类型类以解决所有问题。这些机会将以其他编程语言编写时很少采用的方式自然地发展。我希望这是我的建议,希望五年前有人能把我的脑袋砸碎。