为什么在Haskell中丢弃值是()而不是⊥?

时间:2012-06-18 04:29:50

标签: haskell

在Haskell中,如果有一个值被丢弃,会使用()代替吗?

示例(目前无法真正考虑除IO操作之外的任何事情):

mapM_ :: (Monad m) => (a -> m b) -> [a] -> m ()
foldM_ :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m ()
writeFile :: FilePath -> String -> IO ()

在严格评估下,这很有道理,但在Haskell中,它只会使域名更大。

也许有“未使用的参数”函数d -> ad上是严格的(其中d是一个无约束的类型参数,并且在a中不显示)?例如:seqconst' x y = y seq x

6 个答案:

答案 0 :(得分:13)

我认为这是因为你需要指定要丢弃的值的类型。在Haskell-98中,()是显而易见的选择。只要您知道类型为(),您也可以设置值()(假设评估进展得那么远),以防万一有人试图对其进行模式匹配或某事。我认为大多数程序员不喜欢在代码中引入额外的⊥,因为它只是一个额外的陷阱。我当然避免它。

而不是(),可以创建一个无人居住的类型(当然除了))。

{-# LANGUAGE EmptyDataDecls #-}

data Void

mapM_ :: (Monad m) => (a -> m b) -> [a] -> m Void

现在甚至不可能进行模式匹配,因为没有Void构造函数。我怀疑这种情况不常发生的原因是因为它不兼容Haskell-98,因为它需要EmptyDataDecls扩展。

编辑:你无法在Void上进行模式匹配,但seq会破坏你的一天。感谢@sacundim指出这一点。

答案 1 :(得分:10)

嗯,底部类型字面意思是一个无终止的计算,单位类型就是它 - 一个居住着单一值的类型。很明显,monadic计算通常意味着完成,所以让它们返回undefined是没有意义的。而且,当然,这只是一个安全措施 - 就像John L所说,如果有人模式匹配monadic结果怎么办?因此,monadic计算返回'最低'(在Haskell 98中)类型 - 单位。

答案 2 :(得分:9)

所以,也许我们可以有以下签名:

mapM_     :: (Monad m) => (a -> m b) -> [a] -> m z
foldM_    :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m z
writeFile :: FilePath -> String -> IO z

我们会重新实现相关功能,以便任何绑定zm z IO z的尝试都会将变量绑定到undefined或任何其他底部。

我们获得了什么?现在人们可以编写强制这些计算undefined结果的程序。这是一件好事吗?所有这一切都意味着人们现在可以编写无法正常终止的程序,这是以前无法编写的。

答案 3 :(得分:8)

你在类型和价值观之间感到困惑。

writeFile :: FilePath -> String -> IO ()中,()是单位类型。通过在do块中执行x获得x <- writeFile foo bar的值是(通常) (),这是唯一的非底层居民输入()

OTOH是。由于是每种类型的成员,因此它也可用作类型()的值。如果您在不使用它的情况下丢弃x以上(我们通常甚至不将其提取到变量中),那么它很可能是并且您永远不会知道。从那个意义上说,你已经拥有了你想要的东西如果您正在编写一个您希望始终忽略其结果的函数,则可以使用。但由于是每种类型的值,因此没有类型,因此没有类型IO ⊥

但实际上,它们代表了不同的概念性事物。类型()是包含零信息的值的类型(这就是为什么只有一个值;如果有两个或更多值,那么()值将包含至少与值相同的信息Bool)。 IO ()是生成没有信息的值的IO操作类型,但可能会因生成非信息性值而产生影响。

在某种意义上是非价值的。 1 `div` 0给出,因为没有值可以用作满足整数除法的表达式的结果。抛出异常会产生,因为包含异常抛出的函数不会为您提供其类型的值。非终止给出,因为表达式永远不会以值终止。 是一种处理所有这些非值的方法,就好像它们是某些用途的值。据我所知,它主要是有用的,因为Haskell的懒惰意味着和包含(即[⊥])的数据结构是可区分的。

()与我们使用的情况不同。 writeFile foo bar没有像return $ 1 `div` 0这样的“不可能的值”,它的值中没有信息(除了monadic结构中包含的信息之外)。我可以通过()获得的x <- writeFile foo bar完全明白我可以做的事情;他们只是不是很有趣,所以没有人做过。这明显不同于x <- return $ 1 `div` 0,其中使用该值做任何事情都必须给我另一个定义不明确的值。

答案 4 :(得分:1)

(),即unit type,而不是bottom type)。最大的区别是单元类型是有人居住的,所以它有一个值(Haskell中的()),另一方面,底层类型是无人居住的,所以你不能写这样的函数: / p>

absurd : ⊥
absurd = -- no way

当然你可以在Haskell中做到这一点,因为“底部类型”(当然没有这样的东西)在这里居住着undefined。这使得Haskell不一致。

这样的功能:

disprove : a → ⊥
disprove x = -- ...

可写,与

相同
disprove : ¬ a
disprove x = -- ...

即。它反驳了a类型,因此a是荒谬的。

在任何情况下,您都可以看到单位类型在不同语言中的使用方式,如Haskell中的() :: (),ML中的() : unit,Scala中的() : Unittt : ⊤在阿格达。在像Haskell和Agda这样的语言中(使用IO monad),像putStrLn这样的函数应该有一个类型String → IO ⊤,而不是String → IO ⊥,因为这是荒谬的(逻辑上它表明没有字符串)可以打印,这是不对的。)


免责声明:以前的文字使用Agda表示法,它更多地是关于Agda而不是Haskell。

如果我们有Haskell

data Void

这并不意味着Void无人居住。它居住着undefined,非终止程序,错误和例外。例如:

data Void

instance Show Void where
  show _ = "Void"

data Identity a = Identity { runIdentity :: a }

mapM__ :: (a -> Identity b) -> [a] -> Identity Void
mapM__ _ _ = Identity undefined

然后

print $ runIdentity $ mapM__ (const $ Identity 0) [1, 2, 3]
-- ^ will print "Void".
case runIdentity $ mapM__ (const $ Identity 0) [1, 2, 3] of _ -> print "1"
-- ^ will print "1".
let x = runIdentity $ mapM__ (const $ Identity 0) [1, 2, 3]
x `seq` print x
-- ^ will thrown an exception.

但这并不意味着Void是⊥。所以

mapM_ :: Monad m => (a -> m b) -> [a] -> m Void

其中Void被称为空数据类型,没问题。但

mapM_ :: Monad m => (a -> m b) -> [a] -> m ⊥

是不存在的,但在Haskell中没有⊥这样的类型。

答案 5 :(得分:1)

我想指出一个严重的缺点,写一个特定的形式的返回⊥:如果你写这样的类型,你会得到糟糕的程序:

mapM_ :: (Monad m) => (a -> m b) -> [a] -> m z

这是方式过于多态。例如,请考虑forever :: Monad m => m a -> m b。很久以前我遇到了这个问题,我仍然很痛苦:

main :: IO ()
main = forever putStrLn "This is going to get printed a lot!"

错误显而易见:缺少括号。

  • typechecks 。这正是类型系统应该容易捕获的那种错误。
  • 在运行时静默无限循环(不打印任何内容)。调试很痛苦。

为什么呢?好吧,因为r ->是一个单子。因此,m b几乎与任何匹配。例如:

forever :: m a -> m b
forever putStrLn :: String -> b
forever putStrLn "hello!" :: b -- eep!
forever putStrLn "hello" readFile id flip (Nothing,[17,0]) :: t -- no type error.

这种事情让我认为forever应该键入m a -> m Void