如何将 IO 动作变成 "pure" 函数
How to turn IO Actions into a "pure" function
Haskell 这里是新手。
我手头有一个高阶函数 myTransform
,它接受一个函数 fn :: String -> String
并做一些奇特的事情。
让我们假设实现是
myTransform :: Int -> (String -> String) -> String -> [String]
myTransform n f = take n . iterate f
现在我想改造一个外部程序,如果我没理解错的话,就是一个IO action。签名最好是 String -> IO String
:
import System.Process
externProg :: String -> IO String
externProg s = readProcess "echo" ["-n", s, "+ 1"] ""
问题是,有没有什么方法可以将这个 String -> IO String
函数放入那个 String -> String
参数槽中,而不改变,甚至不知道 myTransform
是如何实现的?
这是一个常见的骗局,但我有时间...
不,您应该 运行 IO 操作,从而获得传递给您的 myTransform
.
的 String
类型值
例如:
main :: IO ()
main =
do stdout <- externProg "myProg" -- "execute" the IO action and obtain "stdout :: String"
let res = myTransform stdout -- res :: String
putStrLn res
或者一旦您熟悉了这门语言并且您对风格没问题:
main :: IO ()
main = putStrLn . myTransform =<< externProg "myProg"
不,你不能。您将必须制作 myTransform 的单子版本。习惯上附加一个大写的 M. I.e.地图变成地图M。 fold becomse foldM ...不幸的是没有iterateM。因此,我会跳过 iterateM
并直接实施它。
myTransformM' :: (Monad m) => Int -> (String -> m String) -> String -> m [String]
myTransformM' 0 f str = return [str]
myTransformM' n f str = do
results <- myTransformM (n-1) f str
next <- f (head results)
return (next:results)
myTransformM n f str = do
results <- myTransformM' n f str
return $ reverse results
您可能会注意到第一个函数的结果顺序相反。这是为了避免函数是二次函数。
您可以自己尝试如果实施 iterateM
会发生什么。它只会永远循环。这是因为 Haskell 永远不知道您是否真的会得到一个列表,或者是否会在某个地方出现 IOError。类似地,如果你使用 Maybe monad,Haskell 永远不会知道你是否真的得到了 Just list
或者如果在路上的某个地方有一个 Nothing
.
iterateM :: (Monad m) => (a -> m a) -> a -> m [a]
iterateM f a = do
result <- f a
results <- iterateM f result
return (result:results)
yokto的回答很好的说明了问题。我只会再添加一个例子。
考虑这个函数:
f1 :: (Int -> Int) -> Int
f1 g = g 10 + g 20
f2 :: (Int -> Int) -> Int
f2 g = g 20 + g 10
这些函数具有完全相同的语义。 Haskell 实现可以根据需要将第一个重写为第二个,而不影响结果。
现在,考虑
myG :: Int -> IO Int
myG x = print x >> return x
main :: IO ()
main = do
x <- f1 myG -- assume this could be made to work, somehow
print x
这应该打印什么?直觉上,它打印
10
20
30
或
20
10
30
取决于 f1
中使用的评估顺序。这很糟糕,因为 f2
也可以使用,它应该会导致相同的结果,但很可能会导致另一个结果。更糟糕的是,编译器可以将一个优化为另一个,因此无法真正保证任何特定的输出:编译器可以随心所欲地更改它。
这是一个大问题,Haskell 旨在避免。 IO 效果的顺序必须在程序中完全指定。为此,必须防止程序员将 IO 内容(如 Int -> IO Int
)转换为非 IO 内容(如 Int -> Int
)。
如果我们为 f
使用 monadic 类型,如
f3 :: (Int -> IO Int) -> IO Int
f3 g = ...
然后 Haskell 将迫使我们指定 g 10
和 g 20
之间的顺序。
Haskell 这里是新手。
我手头有一个高阶函数 myTransform
,它接受一个函数 fn :: String -> String
并做一些奇特的事情。
让我们假设实现是
myTransform :: Int -> (String -> String) -> String -> [String]
myTransform n f = take n . iterate f
现在我想改造一个外部程序,如果我没理解错的话,就是一个IO action。签名最好是 String -> IO String
:
import System.Process
externProg :: String -> IO String
externProg s = readProcess "echo" ["-n", s, "+ 1"] ""
问题是,有没有什么方法可以将这个 String -> IO String
函数放入那个 String -> String
参数槽中,而不改变,甚至不知道 myTransform
是如何实现的?
这是一个常见的骗局,但我有时间...
不,您应该 运行 IO 操作,从而获得传递给您的 myTransform
.
String
类型值
例如:
main :: IO ()
main =
do stdout <- externProg "myProg" -- "execute" the IO action and obtain "stdout :: String"
let res = myTransform stdout -- res :: String
putStrLn res
或者一旦您熟悉了这门语言并且您对风格没问题:
main :: IO ()
main = putStrLn . myTransform =<< externProg "myProg"
不,你不能。您将必须制作 myTransform 的单子版本。习惯上附加一个大写的 M. I.e.地图变成地图M。 fold becomse foldM ...不幸的是没有iterateM。因此,我会跳过 iterateM
并直接实施它。
myTransformM' :: (Monad m) => Int -> (String -> m String) -> String -> m [String]
myTransformM' 0 f str = return [str]
myTransformM' n f str = do
results <- myTransformM (n-1) f str
next <- f (head results)
return (next:results)
myTransformM n f str = do
results <- myTransformM' n f str
return $ reverse results
您可能会注意到第一个函数的结果顺序相反。这是为了避免函数是二次函数。
您可以自己尝试如果实施 iterateM
会发生什么。它只会永远循环。这是因为 Haskell 永远不知道您是否真的会得到一个列表,或者是否会在某个地方出现 IOError。类似地,如果你使用 Maybe monad,Haskell 永远不会知道你是否真的得到了 Just list
或者如果在路上的某个地方有一个 Nothing
.
iterateM :: (Monad m) => (a -> m a) -> a -> m [a]
iterateM f a = do
result <- f a
results <- iterateM f result
return (result:results)
yokto的回答很好的说明了问题。我只会再添加一个例子。
考虑这个函数:
f1 :: (Int -> Int) -> Int
f1 g = g 10 + g 20
f2 :: (Int -> Int) -> Int
f2 g = g 20 + g 10
这些函数具有完全相同的语义。 Haskell 实现可以根据需要将第一个重写为第二个,而不影响结果。
现在,考虑
myG :: Int -> IO Int
myG x = print x >> return x
main :: IO ()
main = do
x <- f1 myG -- assume this could be made to work, somehow
print x
这应该打印什么?直觉上,它打印
10
20
30
或
20
10
30
取决于 f1
中使用的评估顺序。这很糟糕,因为 f2
也可以使用,它应该会导致相同的结果,但很可能会导致另一个结果。更糟糕的是,编译器可以将一个优化为另一个,因此无法真正保证任何特定的输出:编译器可以随心所欲地更改它。
这是一个大问题,Haskell 旨在避免。 IO 效果的顺序必须在程序中完全指定。为此,必须防止程序员将 IO 内容(如 Int -> IO Int
)转换为非 IO 内容(如 Int -> Int
)。
如果我们为 f
使用 monadic 类型,如
f3 :: (Int -> IO Int) -> IO Int
f3 g = ...
然后 Haskell 将迫使我们指定 g 10
和 g 20
之间的顺序。