为什么我不能使用IO构造函数 [英] Why can't I use IO constructor
问题描述
为什么我不能这样做:
import Data.Char
getBool = do
c< - getChar
if c =='t'
then IO True
else IO False
而不是使用 return
?
背景
我会回答稍宽(更有趣)的问题。这是因为至少从语义的角度来看,存在多个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屋!