我刚开始看 Haskell(我以前的 FP 经验是在 Scheme 中),我遇到了这段代码:
do { putStrLn "ABCDE" ; putStrLn "12345" }
对我来说,这是过程编程,如果有的话——尤其是因为副作用的连续性。
有人可以解释一下这段代码在任何方面是如何“起作用的”吗?
我刚开始看 Haskell(我以前的 FP 经验是在 Scheme 中),我遇到了这段代码:
do { putStrLn "ABCDE" ; putStrLn "12345" }
对我来说,这是过程编程,如果有的话——尤其是因为副作用的连续性。
有人可以解释一下这段代码在任何方面是如何“起作用的”吗?
虽然它看起来是一个过程程序,但上面的语法被翻译成一个函数式程序,如下所示:
do { putStrLn "ABCDE" ; putStrLn "12345" }
=>
IO (\ s -> case (putStrLn "ABCDE" s) of
( new_s, _ ) -> case (putStrLn "12345" new_s) of
( new_new_s, _) -> ((), new_new_s))
也就是说,一系列嵌套函数具有唯一的世界参数,它们通过它们线程化,“按程序”对原始函数的调用进行排序。此设计支持将命令式编程编码为函数式语言。
对这种设计背后的语义决策的最佳介绍是“The Awkward Squad”论文,
我认为我们不能清楚地回答这个问题,因为“功能”是一个模糊的概念,而且对于它的含义存在矛盾的想法。所以我更喜欢 Peter Landin 建议的替代术语“外延”,它是精确和实质性的,对我来说,它是函数式编程的核心和灵魂,是什么使它有利于等式推理。有关Landin 定义的一些提示,请参阅这些注释。IO
不是外延的。
这样想吧。它实际上并没有“执行” IO 指令。IO monad 是一个纯值,它封装了要完成的“命令式计算”(但它实际上并没有执行它)。您可以使用 monad 运算符和诸如“do”之类的结构以纯粹的方式将 monad(计算)组合成一个更大的“计算”。尽管如此,没有任何东西本身是“执行”的。事实上,在某种程度上,Haskell 程序的全部目的是组合一个大的“计算”,即它的main
值(具有 type IO a
)。当你运行程序时,运行的是这个“计算”。
这是一个单子。阅读do-notation以了解封面背后发生的事情。
有人可以解释一下这段代码
do { putStrLn "ABCDE" ; putStrLn "12345" }
在任何方面都是“功能性”的吗?
这就是我对 Haskell 中 I/O 现状的看法;通常的免责声明适用>_<
现在(2020 年 6 月),I/O 的“功能性”如何取决于您的 Haskell实现。但情况并非总是如此——事实上,Haskell语言的原始 I/O 模型确实是函数式的!
是时候回到 Haskell 的早期了,在 Philip Wadler 的How to Declare an Imperative的帮助下:
import Prelude hiding (IO)
import qualified Prelude (IO)
import Control.Concurrent.Chan(newChan, getChanContents, writeChan)
import Control.Monad((<=<))
-- pared-back emulation of retro-Haskell I/O
--
runDialogue :: Dialogue -> Prelude.IO ()
runDialogue d =
do ch <- newChan
l <- getChanContents ch
mapM_ (writeChan ch <=< respond) (d l)
respond :: Request -> Prelude.IO Response
respond Getq = fmap Getp getChar
respond (Putq c) = putChar c >> return Putp
main = runDialogue (retro_main :: Dialogue)
{-
implementation side
-----------------------------------
========== retro-Haskell ==========
-----------------------------------
language side
-}
-- pared-back definitions for retro-Haskell I/O
-- from page 14 of Wadler's paper
--
data Request = Getq | Putq Char
data Response = Getp Char | Putp
type Dialogue = [Response] -> [Request]
(将它扩展到所有的复古 Haskell I/O 留给非常热心的读者作为练习 ;-)
你去吧:简单的“ ol' school ”功能I/O!响应流式传输到,然后将请求流回:main
retro_main
凭借所有经典的优雅,您可以愉快地定义:
-- from page 15 of Wadler's paper
echoD :: Dialogue
echoD p =
Getq :
case p of
Getp c : p' ->
if (c == '\n') then
[]
else
Putq c :
case p' of
Putp : p'' -> echoD p''
你看起来很困惑——没关系;你会得到它的窍门:-D
这是A History of Haskell的第 24 页中的一个更复杂的示例:
{- 主要〜(成功:〜((Str userInput):〜(成功:〜(r4:_)))) = [ AppendChan stdout "输入文件名\n", ReadChan 标准输入, AppendChan 标准输出名称, 读取文件名, AppendChan 标准输出 (案例 r4 的 Str 内容 -> 内容 失败 ioerr ->“无法打开文件”) ] where (name : _) = lines userInput -}
你还在吗?
你旁边是垃圾桶吗?嗯?你病了?该死。
那么好吧 - 也许你会发现使用更易识别的界面会更容易一些:
-- from page 12 of Wadler's paper
--
echo :: IO ()
echo = getc >>= \ c ->
if (c == '\n') then
done
else
putc c >>
echo
-- from pages 3 and 7
--
puts :: String -> IO ()
puts [] = done
puts (c:s) = putc c >> puts s
done :: IO ()
done = return ()
-- based on pages 16-17
--
newtype IO a = MkIO { enact :: Reality -> (Reality, a) }
type Reality = ([Response], [Request])
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO m k = MkIO $ \ (p0, q2) -> let ((p1, q0), x) = enact m (p0, q1)
((p2, q1), y) = enact (k x) (p1, q2)
in
((p2, q0), y)
unitIO :: a -> IO a
unitIO x = MkIO $ \ w -> (w, x)
putc :: Char -> IO ()
putc c = MkIO $ \ (p0, q1) -> let q0 = Putq c : q1
Putp : p1 = p0
in
((p1, q0), ())
getc :: IO Char
getc = MkIO $ \ (p0, q1) -> let q0 = Getq : q1
Getp c : p1 = p0
in
((p1, q0), c)
mainD :: IO a -> Dialogue
mainD main = \ p0 -> let ((p1, q0), x) = enact main (p0, q1)
q1 = []
in
q0
-- making it work
instance Monad IO where
return = unitIO
(>>=) = bindIO
我还包含了您的示例代码;也许这会有所帮助:
-- local version of putStrLn
putsl :: String -> IO ()
putsl s = puts s >> putc '\n'
-- bringing it all together
retro_main :: Dialogue
retro_main = mainD $ do { putsl "ABCDE" ; putsl "12345" }
是的:这仍然是简单的函数式 I/O;检查的类型retro_main
。
显然,基于对话的 I/O 最终与空间站中的臭鼬一样受欢迎。把它塞进一个单子界面只会把恶臭(和它的来源)限制在车站的一小部分——到那时,Haskellers 想要那个臭臭的东西消失了!
因此,Haskell 中用于 I/O 的抽象单子接口成为标准——那一小部分和它刺鼻的乘客被从空间站中分离出来,并被拖回地球,那里的新鲜空气更加丰富。空间站的气氛有所改善,大多数哈斯克勒人继续做其他事情。
但有些人对这种新的、抽象的 I/O 模型感到担忧——Haskell 不再起作用了吗?
好吧,如果模型是基于抽象的,在这种情况下:
IO
return
(>>=)
、catch
等getArgs
, getEnv
, 等那么这些实体的实际定义方式将特定于 Haskell 的每个实现。现在应该问的是:
所以你的问题的答案:
有人可以解释一下这段代码
do { putStrLn "ABCDE" ; putStrLn "12345" }
在任何方面都是“功能性”的吗?
现在取决于您使用的是哪种 Haskell 实现。
不是你想要的答案?现在,要么是这个,要么是臭鼬......
它不是功能代码。为什么会这样?