haskell 采用 'pattern' 的高阶函数

haskell higher order function that takes a 'pattern'

开始

所以我有

> 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 的一个别名,所以这应该有用吗?!


最后我意识到 'alias' 是关键词,所以...我没有使用数据而是使用类型,我想我可以回答我自己的问题..

所以我的模块变成

> 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

建议?我猜我可能需要查看类型 类?

如您判断正确,原来报错的原因是:

data HanoiDisk = HanoiDisk (Maybe Integer)

引入了 "algebraic data type" 而不是 "type alias",因此 ab 的值属于 HanoiDisk 而不是 Maybe Integer然后导致类型错误,因为类型 HanoiDisk 不是 Maybe a and/or Maybe b 所需的 applyMaybe.

形式

完成你想要的事情的一种方法——让一个通用的 applyMaybe 对不是 Maybe a 但某种程度上 "look" 类似于模式 Maybe a 的类型进行操作 - - 就是引入一个类型class,而是套用一个关于正则表达式的老笑话:"A new Haskell programmer has a problem and decides to use a type class. Now the programmer has two problems."呵呵!

直接转换

我在下面包含了一个 class 类型的解决方案,但是像 Maybe Integer 一样处理 HanoiDisk 的更直接和惯用的方法是提供一个转换函数,显式或通过在数据类型中引入命名字段,如下所示。这种方法在整个标准库和现实世界中使用 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 视为在道德上等同于在您编写 mean xs = sum xs / fromIntegral (length xs) 时必须使用 fromIntegral。它只是满足 Haskell 的要求,即使 "obvious" 类型转换也应该是显式的。

这种方法的另一个优点是——如评论中所述——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解决方案

无论如何,如果您真的想要使用类型class来做到这一点,它将看起来像这样。您将定义一个 class 允许 "maybe-like" 类型与实际可能值之间的转换:

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 表示形式不是一个好选择。当您不再编写泛型函数并输入 classes 并尝试解决您的实际编程问题时,这对您来说可能会变得很明显。例如,您可以这样写:

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

或通过 "obviously" 创建有效磁盘的其他代码。

在这一点上,您还会意识到,您花在编写类型 classes、实例和通用 applyMaybe 函数上的所有时间都被浪费了。

如果您坚持采用更灵活的设计,并随处散布一些 getHanoiDisk 调用,那么您可以放弃的无用代码就会少很多。

如果您有 Java 背景或其他背景,您可能已经习惯了在编写之前在前端开发精心设计的样板、对象层次结构和最佳实践设计模式的想法你的第一行有用的代码。这在 Java 宇宙中可能是一种有效的方法,但在编程中效果较差 Haskell,尤其是当您刚开始时。

虽然这会很困难,但请有意识地努力编写尽可能简单的代码,不要在编译时寻找人为的机会来捕获非正数,编写通用的高阶函数,并引入 type classes 来解决每一个问题。这些机会将以一种在用其他编程语言编写时很少出现的方式自然演变。这是我希望有人在五年前敲入我脑海的建议。