什么是返回函数的函数?
你快到了:
因此,组合函数相当于构建更高维的查找表。
这是一个小例子,在 Haskell 中:
infixr 2 ||
(||) :: Bool -> (Bool -> Bool)
True || True = True
True || False = True
False || True = True
False || False = False
然后您的查找表将采用 case-expression 的形式:
x || y = case (x, y) of (True, True) -> True
(True, False) -> True
(False, True) -> True
(False, False) -> False
而不是使用元组:
x || y = case x of True -> (case y of True -> True
False -> True)
False -> (case y of True -> True
False -> False)
如果我们现在将参数y 移动到新的本地函数中:
(||) x = case x of True -> let f y = case y of True -> True
False -> True
in f
False -> let g y = case y of True -> True
False -> False
in g
那么对应的map-of-maps将是:
+-------+-----------------------+
| x | (||) x |
+-------+-----------------------+
| True | |
| | +-------+-------+ |
| | | y | f y | |
| | +-------+-------+ |
| | | True | True | |
| | +-------+-------+ |
| | | False | True | |
| | +-------+-------+ |
| | |
+-------+-----------------------+
| False | |
| | +-------+-------+ |
| | | y | g y | |
| | +-------+-------+ |
| | | True | True | |
| | +-------+-------+ |
| | | False | False | |
| | +-------+-------+ |
| | |
+-------+-----------------------+
因此,您的抽象模型可以扩展到高阶函数 - 它们只是从某个域映射到由其他映射组成的共同域。
什么是返回 I/O 动作的函数(如 Haskell 的 IO 类型)?
这是一个有趣的事实:部分应用的函数类型:
forall a . (->) a
是一元的:
unit :: a -> (d -> a)
unit x = \ u -> x
bind :: (d -> a) -> (a -> (d -> b)) -> (d -> b)
bind m k = \ u -> let x = m u in k x u
instance Monad ((->) a) where
return = unit
(>>=) = bind
这多么简单!要是IO 类型能这么容易定义就好了……
当然不可能完全相同 - 涉及外部交互 - 但我们能接近多远?
好吧,I/O 通常需要以某种预定义的顺序发生才能发挥作用(例如,抓住房子钥匙然后离开锁定的房子),因此需要一种机制来按顺序对 @ 的评估进行排序987654337@ 表达式 - bang patterns 怎么样?
unit :: a -> (d -> a)
unit x = \ u -> x
bind :: (d -> a) -> (a -> (d -> b)) -> (d -> b)
bind m k = \ u -> let !x = m u in k x u
几乎不引人注意 - 很好!作为奖励,我们现在还可以为(>>) 提供一个有用的定义:
next :: (d -> a) -> (d -> b) -> (d -> b)
next m w = \ u -> let !_ = m u in w u
instance Monad ((->) a) where
.
.
.
(>>) = next
让我们考虑以下小型 Haskell 2010 程序:
main :: IO ()
main = putStr "ha" >> putStr "ha" >> putStr "!\n"
这可以改写为:
main = let x = putStr "ha" in x >> x >> putStr "!\n"
假设适当的定义:
puts :: String -> (d -> ())
putc :: Char -> (d -> ())
我们也可以重写吗:
main' :: d -> ()
main' = puts "ha" >> puts "ha" >> puts "!\n"
作为:
main' = let x = puts "ha" in x >> x >> puts "!\n"
否 - 引用 Philip Wadler 的 How to Declare an Imperative:
[...] 笑点在我们身上:程序只打印一个 "ha",此时变量 x 被绑定。在存在副作用的情况下,最简单形式的equational reasoning 将失效。
(第 5 页第 2.2 节。)
为什么?让我们看看发生了什么变化:
let x = puts "ha" in x >> x
如果(>>) 被替换为它的定义:
let x = puts "ha" in \ u -> let !_ = x u in x u
原因已揭示 - 虽然 x u 被使用了两次,但它只被评估一次,因为 Haskell 是 nonstrict - 第二次使用 x u 只是检索第一个结果。
这是一个合法的转换,例如:
testme n = n^2 + n^2 + n
和:
testme n = let x = n^2 in x + x + n
和优化像 GHC 这样的 Haskell 实现依赖于它和许多其他转换来完成他们的目标 - 将 I/O 视为一些特殊情况很可能是完全徒劳的练习......让我们修改代码让它赢'最终不会被重写。
一种简单的方法是让所有对puts 或putc 的调用都是唯一的,基于F. Warren Burton 在Nondeterminism with Referential Transparency in Functional Programming Languages 中描述的技术:
let x = puts "ha" in \ u -> let !u1:u2:_ = ... in
let !_ = x u1 in x u2
因此:
bind :: (d -> a) -> (a -> (d -> b)) -> (d -> b)
bind m k = \ u -> let !u1:u2:_ = ... in
let !x = m u1 in
k x u2
next :: (d -> a) -> (d -> b) -> (d -> b)
next m w = \ u -> let !u1:u2:_ = ... in
let !_ = m u1 in
w u2
但是,这还不够:
let x = puts "ha" in \ u -> let !u1:u2:_ = ... in
let !_ = x u1 in x u
我们可以从 Clean 获得提示并添加唯一性类型,但已经进行了一项重大更改(爆炸模式扩展) - 我们真的要添加另一个扩展吗什么时候遇到新问题?
我们不妨做一个completely-new programming language...
继续,让我们重命名所有烦人的d 类型变量,以及puts 和putc:
data OI
putstr :: String -> OI -> ()
putchar :: Char -> OI -> ()
嗯...所有输出,没有输入:
getchar :: OI -> Char
其他定义呢?让我们试试吧:
next :: (OI -> a) -> (IO -> b) -> OI -> b
next m w = \ u -> let !u1:u2:_ = ... in
let !_ = m u1 in
w u2
所以u、u1和u2具有相同的类型;它们是相关的:
next :: (OI -> a) -> (IO -> b) -> OI -> b
next m w = \ u -> let !u1:u2:_ = parts u in
let !_ = m u1 in
w u2
像parts 这样的名字相当通用:
class Partible a where
parts :: a -> [a]
partsOI :: OI -> [OI]
instance Partible OI where
parts = partsOI
我们现在可以为putstr 提供定义:
putstr s = \ u -> foldr (\!_ -> id) () $ zipWith putchar s $ parts u
并完成bind的:
bind :: (OI -> a) -> (a -> OI -> b) -> OI -> b
bind m k = \ u -> let !u1:u2:_ = parts u in
let !x = m u1 in
k x u2
unit的那个定义:
unit :: a -> OI -> a
unit x = \ u -> x
不使用它的参数u,所以:
let x = puts "ha" in \ u -> let !u1:u2:_ = ... in
let !_ = x u1 in unit () u
是可能的 - 这怎么比:
let x = puts "ha" in \ u -> let !u1:u2:_ = ... in
let !_ = x u1 in x u
unit 也应该打电话给parts吗?
unit x = \ u -> let !_:_ = parts u in x
现在unit、bind 和next 执行的第一项任务涉及partsOI 的(间接)应用...如果OI 值被破坏 partsOI 首次使用时,无法重复使用?
否:不只是partsOI,还有putchar 和getchar - 那么这三个都可以使用一个共同的检查和破坏 机制;然后可以将 OI 参数的重用视为无效,例如通过抛出异常或引发错误(就像现在在 Haskell 中处理除零一样)。
现在,要么是那个,要么是唯一性类型......
在评估期间破坏 OI 值会排除惯用的 Haskell 类型声明。就像Int 或Char 一样,OI 需要预先定义;与partsOI、putchar和getchar一起构成抽象数据类型。
一些观察:
返回一对 OI 值很简单:
part u :: Partible a => a -> (a, a)
part u = let !u1:u2:_ = parts u in (u1, u2)
这很有趣:
parts u = let !(u1, u2) = part u in u1 : part u
这表明:
class Partible a where
part :: a -> (a, a)
parts :: a -> [a]
-- Minimal complete definition: part or parts
part u = let !u1:u2:_ = parts u in (u1, u2)
parts u = let !(u1, u2) = part u in u1 : part u
partOI :: OI -> (OI, OI)
instance Partible OI where
part = partOI
还有:
unit :: a -> OI -> a
unit x = \ u -> let !(_, _) = part u in x
bind :: (OI -> a) -> (a -> OI -> b) -> OI -> b
bind m k = \ u -> let !(u1, u2) = part u in
let !x = m u1 in
k x u2
next :: (OI -> a) -> (IO -> b) -> OI -> b
next m w = \ u -> let !(u1, u2) = part u in
let !_ = m u1 in
w u2
效果很好!还有一个细节:main main' - 当它被调用时会发生什么?
这一切都在类型签名中:
main' :: OI -> ()
一个实现会将main' 的应用评估为一个新的OI 值,然后丢弃结果; OI 值是通过类似于 partOI 用于生成它返回的 OI 值的机制获得的。
是时候把所有东西放在一起了:
-- the OI ADT:
data OI
putchar :: Char -> OI -> ()
getchar :: OI -> Char
partOI :: OI -> (OI, OI)
class Partible a where
part :: a -> (a, a)
parts :: a -> [a]
-- Minimal complete definition: part or parts
part u = let !u1:u2:_ = parts u in (u1, u2)
parts u = let !(u1, u2) = part u in u1 : part u
instance Partible OI where
part = partOI
putstr :: String -> OI -> ()
putstr s = \ u -> foldr (\!_ -> id) () $ zipWith putchar s $ parts u
unit :: a -> OI -> a
unit x = \ u -> let !(_, _) = part u in x
bind :: (OI -> a) -> (a -> OI -> b) -> OI -> b
bind m k = \ u -> let !(u1, u2) = part u in
let !x = m u1 in
k x u2
next :: (OI -> a) -> (IO -> b) -> OI -> b
next m w = \ u -> let !(u1, u2) = part u in
let !_ = m u1 in
w u2
instance Monad ((->) OI) where
return = unit
(>>=) = bind
(>>) = next
{- main' :: OI -> () -}
那么...问题是什么?
什么是返回 I/O 操作的函数(如 Haskell 的 IO 类型)?
我只回答简单的问题:
什么是 I/O 操作(如 Haskell 的 IO 类型)?
在我看来,I/O 操作(Haskell 中的 IO 值)是一个抽象实体,它承载着函数类型,其域是特定于目的的可分割类型外部互动。
P.S:如果你想知道我为什么不使用 I/O 的 pass-the-planet 模型:
newtype IO' a = IO' (FauxWorld -> (FauxWorld, a))
data FauxWorld = FW OI
instance Monad IO' where
return x = IO' $ \ s@(FW _) -> (s, x)
IO' m >>= k = IO' $ \ s@(FW _) -> let !(s', x) = m s in
let !(IO' w) = k x in
w s'
putChar' :: Char -> IO' ()
putChar' c = IO' $ \ (FW u) -> let !(u1, u2) = part u in
let !_ = putchar c u1 in
(FW u2, ())
putStr' :: String -> IO' ()
putStr' s = IO' $ \ (FW u) -> let !(u1, u2) = part u in
let !_ = putstr s u1 in
(FW u2, ())
getChar' :: IO' Char
getChar' = IO' $ \ (FW u) -> let !(u1, u2) = part u in
let !c = getchar u1 in
(FW u2, c)