创建 Haskell 'init'函数的安全版本



我正在研究"现实世界的哈斯克尔",任务是制作head, tail, last,的安全版本,init.我在前三个上取得了成功,但Maybe类型类在init上杀死了我。

这是我的代码:

-- safeInit
safeInit    ::  [a] ->  Maybe [a]
safeInit []     =   Nothing
safeInit (x:xs) =   if null xs
then Just [x]
else x : (safeInit xs)

以下是加载到 GHCI 时产生的错误(该函数从原始文件的第 23 行开始:

[1 of 1] Compiling Main             ( ch04.exercises.hs, interpreted )
> ch04.exercises.hs:27:26: error:
>     • Couldn't match expected type ‘Maybe [a]’ with actual type ‘[a]’
>     • In the expression: x : (safeInit xs)
>       In the expression: if null xs then Just [x] else x : (safeInit xs)
>       In an equation for ‘safeInit’:
>           safeInit (x : xs) = if null xs then Just [x] else x : (safeInit xs)
>     • Relevant bindings include
>         xs :: [a] (bound at ch04.exercises.hs:25:13)
>         x :: a (bound at ch04.exercises.hs:25:11)
>         safeInit :: [a] -> Maybe [a] (bound at ch04.exercises.hs:24:1)    | 27 |                     else x : (safeInit xs)    |                
> ^^^^^^^^^^^^^^^^^
> 
> ch04.exercises.hs:27:31: error:
>     • Couldn't match expected type ‘[a]’ with actual type ‘Maybe [a]’
>     • In the second argument of ‘(:)’, namely ‘(safeInit xs)’
>       In the expression: x : (safeInit xs)
>       In the expression: if null xs then Just [x] else x : (safeInit xs)
>     • Relevant bindings include
>         xs :: [a] (bound at ch04.exercises.hs:25:13)
>         x :: a (bound at ch04.exercises.hs:25:11)
>         safeInit :: [a] -> Maybe [a] (bound at ch04.exercises.hs:24:1)    | 27 |                     else x : (safeInit xs)    |                
> ^^^^^^^^^^^ Failed, no modules loaded.

无论如何,我用Just标记或不标记最后两行的xxs,我都会得到不同但非常相关的输入错误。 我缺少将"可能"类型与列表一起使用的哪些微妙之处?

这不起作用的主要原因是因为您的表达式x : safeInit xs不会进行类型检查。事实上,safeInit xs是一个Maybe [a],但是(:)有类型(:) :: a -> [a] -> [a],所以类型不匹配。

还有一个语义错误。如果null xsTrue,那么你应该返回Just []而不是Just [x],因为x是列表中的最后一个元素。

你可以利用fmap :: Functor f => (a -> b) -> f a -> f b(所以对于f ~ Maybefmapfmap :: (a -> b) -> Maybe a -> Maybe b(,来改变包装在Just中的值:

safeInit :: [a] -> Maybe [a]
safeInit [] = Nothing
safeInit [_] = Just []
safeInit (x:xs) =fmap (x:)(safeInit xs)

但这会导致Just中值的大量包装和解包。这也意味着对于无限列表,它将陷入无限循环。我们可以简单地检查列表是否至少包含 on 元素,然后执行 init 逻辑作为我们包装在Just中的函数的结果:

safeInit :: [a] -> Maybe [a]
safeInit [] = Nothing
safeInit (x:xs) =Just (go xs x)
where go [] _ = []
go (x2:xs) x = x : go xs x2

一个有趣的问题是如何用foldr来写safeInit。除了谜题的乐趣之外,这还允许它作为"好消费者"参与GHC中的列表融合优化,这在某些情况下可以提高性能。我们从Willem Van Onsem回答中的第一个(天真(版本开始:

safeInit0 :: [a] -> Maybe [a]
safeInit0 [] = Nothing
safeInit0 [_] = Just []
safeInit0 (x:xs) = fmap (x:) (safeInit0 xs)

这样做的第一个问题是它的形状不太像褶皱:它有单独的[p]p:q:rs的情况。修补此问题的一个经典技巧是传递一个携带列表中前一个值的Maybe

safeInit1 :: [a] -> Maybe [a]
safeInit1 xs0 = go xs0 Nothing
where
-- This first case only happens when
-- the whole list is empty.
go [] Nothing = Nothing
go [] (Just x) = Just [x]
go (x:xs) Nothing = go xs (Just x)
go (x:xs) (Just prev) = (prev:) <$> go xs (Just x)

下一个问题是语义上的:它不适用于无限或部分定义的参数。我们想要

safeInit [1..] = Just [1..]

但在这种情况下safeInit1会有所不同,因为fmapMaybe论证必然是严格的。但事实证明,我们可以使用一些信息:在这种情况下,fmap只会应用于Just值。练习:证明这一点。

我们将利用这一点,以一种奇怪的方式将Maybe [a]表示为(Bool, [a]),其中Nothing表示为(False, [])Just xs表示为(True, xs)。现在我们可以更懒了:

safeInit2 :: [a] -> Maybe [a]
safeInit2 xs = case helper2 xs of
(False, _) -> Nothing
(True, xs) -> Just xs
helper2 :: [a] -> (Bool, [a])
helper2 xs0 = go xs0 Nothing
where
go [] Nothing = (False, [])
go [] _ = (True, [])
go (x:xs) mb = case mb of
Nothing -> (True, rest)
Just p -> (True, p:rest)
where
rest = snd (go xs (Just x))

现在这正好是折叠的形状:

safeInit3 :: [a] -> Maybe [a]
safeInit3 xs = case helper3 xs of
(False, _) -> Nothing
(True, xs) -> Just xs
helper3 :: [a] -> (Bool, [a])
helper3 xs0 = foldr go stop x0 Nothing
where
stop Nothing = (False, [])
stop _ = (True, [])
go x r mb = case mb of
Nothing -> (True, rest)
Just p -> (True, p:rest)
where
rest = snd (r (Just x))

您可能会担心所有这些中间Maybe和对会导致性能问题,但实际上GHC能够将它们全部优化,产生非常类似于Willem Van Onsem的优化实现的东西。

最新更新