为什么我不能使用IO构造函数 [英] Why can't I use IO constructor

查看:129
本文介绍了为什么我不能使用IO构造函数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为什么我不能这样做:

  import Data.Char 

getBool = do
c< - getChar
if c =='t'
then IO True
else IO False

而不是使用 return

解决方案 div>

背景



我会回答稍宽(更有趣)的问题。这是因为至少从语义的角度来看,存在多个IO构造函数。有多种类的 IO 值。我们可以认为打印到屏幕上可能有一种 IO 值,一种是 IO 值从文件中读取,等等。



我们可以想象,为了推理,IO被定义为类似于

 数据IO a = ReadFile a 
| WriteFile a
|网络
| StdOut a
| StdIn a
...
| GenericIO a

每种类型的 IO 行动有。 (但是,请记住,这实际上并不是如何实现 IO IO 是魔术最好的玩法,除非)



现在,有趣的问题 - 为什么他们这样做,以至于我们无法手动构建这些问题?为什么他们没有导出这些构造函数,以便我们可以使用它们?这导致了一个更广泛的问题。



为什么你不想为数据类型导出构造函数?



基本上有两个原因 - 第一个可能是最明显的一个。



1。构造函数也是解构器



如果您有权访问构造函数,还可以访问 de 构造函数, 。想想也许一个类型。如果我给你一个 Maybe 的值,你可以通过模式匹配来提取 Maybe 里面的内容!这很容易。

  getJust :: Maybe a  - > a 
getJust m = case m of
只需x - > x
Nothing - >错误吹起来!

试想一下,如果你可以用 IO 。这意味着 IO 会停止安全。您可以在纯函数中做同样的事情。

  getIO :: IO a  - > a 
getIO io =
的情况io ReadFile s - > s
_ - >错误不是从一个文件,吹起来!

这太可怕了。如果您可以访问 IO 构造函数,则可以创建一个将 IO 值转换为纯值的函数。这太糟糕了。



所以这是不导出数据类型构造函数的一个很好的理由。如果你想保留一些数据秘密,你必须保持你的构造函数的秘密,否则有人可以提取他们想要的任何数据与模式匹配。



2。你不想让任何值



这个原因对于面向对象的程序员来说是熟悉的。当你第一次学习面向对象的编程时,你会发现对象有一个特殊的方法,当你创建一个新的对象时,它会被调用。在这种方法中,您还可以初始化对象内部字段的值,最好的是 - 您可以对这些值执行完整性检查。你可以确保这些值是有意义的,如果它们不是这样的话就抛出异常。

好吧,你可以在Haskell中做同样的事情。假设您是一家拥有少量打印机的公司,并且您想要追踪他们的年龄和位于他们所在建筑的哪个楼层。所以你写了一个Haskell程序。您的打印机可以像这样存储:

  data Printer = Printer {name :: String 
,age :: Int
,floor :: Int
}

现在,您的建筑物只有4个并且你不想意外地说你在14楼有一台打印机。这可以通过不导出打印机构造函数来完成,而是有一个函数 mkPrinter 如果所有参数都有意义,它将为您创建打印机。

  mkPrinter :: String  - > Int  - >可能打印机
mkPrinter名称floor =
如果floor> = 1&& floor(< = 4
)然后Just(打印机名称0 floor)
else Nothing



<如果您导出此 mkPrinter 函数,您知道没有人可以在不存在的地板上创建打印机。


Why can't I do this:

import Data.Char

getBool = do
  c <- getChar
  if c == 't' 
    then IO True 
    else IO False

instead of using return?

解决方案

Background

I'll answer the slightly broader (and more interesting) question. This is because there is, at least from a semantical standpoint, more than one IO constructor. There is more than one "kind" of IO value. We can think that there is probably one kind of IO value for printing to the screen, one kind of IO value for reading from a file, and so on.

We can imagine, for the sake of reasoning, IO being defined as something like

data IO a = ReadFile a
          | WriteFile a
          | Network a
          | StdOut a
          | StdIn a
          ...
          | GenericIO a

with one kind of value for every kind of IO action there is. (However, keep in mind that this is not actually how IO is implemented. IO is magic best not toyed with unless you are a compiler hacker.)

Now, the interesting question – why have they made it so that we can't construct these manually? Why have they not exported these constructors, so that we can use them? This leads into a much broader question.

Why would you want to not export constructors for a data type?

And there are basically two reasons for this – the first one is probably the most obvious one.

1. Constructors are also deconstructors

If you have access to a constructor, you also have access to a de-constructor that you can do pattern matching on. Think about the Maybe a type. If I give you a Maybe value, you can extract whatever is "inside" that Maybe with pattern matching! It's easy.

getJust :: Maybe a -> a
getJust m = case m of
              Just x -> x
              Nothing -> error "blowing up!"

Imagine if you could do this with IO. That would mean IO would stop being safe. You could just do the same thing inside a pure function.

getIO :: IO a -> a
getIO io = case io of
             ReadFile s -> s
             _ -> error "not from a file, blowing up!"

This is terrible. If you have access to the IO constructors, you can create a function that turns an IO value into a pure value. That sucks.

So that's one good reason to not export the constructors of a data type. If you want to keep some of the data "secret", you have to keep your constructors secret, or otherwise someone can just extract any data they want to with pattern matching.

2. You don't want to allow any value

This reason will be familiar to object-oriented programmers. When you first learn object-oriented programming, you learn that objects have a special method that is invoked when you create a new object. In this method, you can also initialise the values of the fields inside the object, and the best thing is – you can perform sanity checking on these values. You can make sure the values "make sense" and throw an exception if they don't.

Well, you can do sort of the same thing in Haskell. Say you are a company with a few printers, and you want to keep track of how old they are and on which floor in the building they are located. So you write a Haskell program. Your printers can be stored like this:

data Printer = Printer { name :: String
                       , age :: Int
                       , floor :: Int
                       }

Now, your building only has 4 floors, and you don't want to accidentally say you have a printer on floor 14. This can be done by not exporting the Printer constructor, and instead having a function mkPrinter which creates a printer for you if all the parameters make sense.

mkPrinter :: String -> Int -> Maybe Printer
mkPrinter name floor =
  if floor >= 1 && floor <= 4
     then Just (Printer name 0 floor)
     else Nothing

If you export this mkPrinter function instead, you know that nobody can create a printer on a non-existing floor.

这篇关于为什么我不能使用IO构造函数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆