我正在努力解决如何在Haskell中进行有状态计算的一般问题。例如。下面的简单算法可以在Python的生成器工具的帮助下表示为有状态但“懒惰”的计算,只执行到达下一个yield
语句所需的步骤,然后将控制流返回给调用者,直到请求下一个元素:
def solveLP(vmax0, elems):
elem_true_ixs = [ [ ei for ei, b in enumerate(row) if b ] for row in elems ]
return go(vmax0, elem_true_ixs)
def go(vmax, mms):
if not mms:
yield []
else:
for ei in mms[0]:
maxcnt = vmax[ei]
if not maxcnt > 0:
continue
vmax[ei] = maxcnt-1 # modify vmax vector in-place
for es in go(vmax, mms[1:]):
# note: inefficient vector-concat operation
# but not relevant for this question
yield [ei]+es
vmax[ei] = maxcnt # restore original vmax state
for sol in solveLP([1,2,3],[[True,True,False],[True,False,True]]):
print sol
# prints [0,2], [1,0], and [1,2]
这可以很容易地转换为惰性Haskell计算(例如,当m
专门用于Logic
或[]
时),例如
import Control.Monad
import qualified Data.Vector.Unboxed as VU
solveLP :: MonadPlus m => VU.Vector Int -> [[Bool]] -> m [Int]
solveLP vmax0 elems = go vmax0 elemTrueIxs
where
-- could be fed to 'sequence'
elemTrueIxs = [ [ ei | (ei,True) <- zip [0::Int ..] row ] | row <- elems ]
go vmax [] = return []
go vmax (m:ms) = do
ei <- mlist m
let vmax' = vmax VU.// [(ei, maxcnt-1)] -- this operation is expensive
maxcnt = vmax VU.! ei
guard $ maxcnt > 0
es <- go vmax' ms
return $ (ei:es)
mlist = msum . map return
...但我希望能够通过使用可变向量和原位修改单个vmax0
向量来接近原始Python实现(因为我只需要递增/递减)单个元素,并且复制整个向量只是为了替换单个元素,这是一个很大的开销,矢量变得越长;请注意,这只是我尝试实现的一类算法的玩具示例
所以我的问题是 - 假设有一种方法可以实现这一点 - 如何在ST monad中表达这样一个有状态算法,同时仍然能够在生成期间将结果返回给调用者计算?我试过通过monad-transformers将ST monad与list monad结合起来,但我无法弄清楚如何让它工作......
答案 0 :(得分:3)
只使用懒惰的ST。在Haskell中,普通旧列表基本上与Python生成器相同,因此我们将返回结果列表(结果为[Int]
)。这是Python代码的音译:
import Control.Monad.ST.Lazy
import Data.Array.ST
import Control.Monad
import Data.List
solveLP :: [Int] -> [[Bool]] -> [[Int]]
solveLP vmax_ elems_ = runST $ do
vmax <- newListArray (0, length vmax_) vmax_
let elems = map (findIndices id) elems_
go vmax elems
go :: STArray s Int Int -> [[Int]] -> ST s [[Int]]
go vmax [] = return [[]]
go vmax (mm:mms) = liftM concat . forM mm $ \ei -> do
maxcnt <- readArray vmax ei
if not (maxcnt > 0) then return [] else do
writeArray vmax ei (maxcnt - 1)
rest <- go vmax mms
writeArray vmax ei maxcnt
return (map (ei:) rest)
尝试例如solveLP [1,undefined,3] [[True,True,False],[True,False,True]]
看到它真的确实会懒散地返回结果。
答案 1 :(得分:2)
我早上太早才能及时了解你的算法。但如果我正确地阅读了基本问题,你可以使用懒惰的ST。这是一个简单的例子:
import Control.Monad.ST.Lazy
import Data.STRef.Lazy
generator :: ST s [Integer]
generator = do
r <- newSTRef 0
let loop = do
x <- readSTRef r
writeSTRef r $ x + 1
xs <- loop
return $ x : xs
loop
main :: IO ()
main = print . take 25 $ runST generator
它正是从ST动作创建一个延迟结果流来维持其状态。
答案 2 :(得分:2)
让我们更直接地翻译Python代码。你在Python中使用协同程序,那么为什么不在Haskell中使用协程呢?然后是可变载体的问题;请参阅下面的详细信息。
首先,大量进口:
-- Import some coroutines
import Control.Monad.Coroutine -- from package monad-coroutine
-- We want to support "yield" functionality like in Python, so import it:
import Control.Monad.Coroutine.SuspensionFunctors (Yield(..), yield)
-- Use the lazy version of ST for statefulness
import Control.Monad.ST.Lazy
-- Monad utilities
import Control.Monad
import Control.Monad.Trans.Class (lift)
-- Immutable and mutable vectors
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Data.Vector.Mutable (STVector)
import qualified Data.Vector.Mutable as Vector
以下是一些实用程序定义,它们可以将协同程序视为在Python中的行为,或多或少:
-- A generator that behaves like a "generator function" in Python
type Generator m a = Coroutine (Yield a) m ()
-- Run a generator, collecting the results into a list
generateList :: Monad m => Generator m a -> m [a]
generateList generator = do
s <- resume generator -- Continue where we left off
case s of
-- The function exited and returned a value; we don't care about the value
Right _ -> return []
-- The function has `yield`ed a value, namely `x`
Left (Yield x cont) -> do
-- Run the rest of the function
xs <- generateList cont
return (x : xs)
现在我们需要以某种方式使用STVector
。您声明要使用 lazy ST,并且STVector
上的预定义操作仅针对严格 ST定义,因此我们需要制作一个几个包装函数。我不是要为这样的东西制作运算符,但如果你真的想让代码pythonic(例如$=
writeLazy
或者其他的话)你就可以了;你需要以某种方式处理索引投影,但是它可能会让它看起来更好看。)
writeLazy :: STVector s a -> Int -> a -> ST s ()
writeLazy vec idx val = strictToLazyST $ Vector.write vec idx val
readLazy :: STVector s a -> Int -> ST s a
readLazy vec idx = strictToLazyST $ Vector.read vec idx
thawLazy :: Vector a -> ST s (STVector s a)
thawLazy = strictToLazyST . Vector.thaw
所有工具都在这里,所以我们只需翻译算法:
solveLP :: STVector s Int -> [[Bool]] -> Generator (ST s) [Int]
solveLP vmax0 elems =
go vmax0 elemTrueIxs
where
elemTrueIxs = [[ei | (ei, True) <- zip [0 :: Int ..] row] | row <- elems]
go :: STVector s Int -> [[Int]] -> Generator (ST s) [Int]
go _ [] = yield []
go vmax (m : ms) = do
forM_ m $ \ ei -> do
maxcnt <- lift $ readLazy vmax ei
when (maxcnt > 0) $ do
lift $ writeLazy vmax ei $ maxcnt - 1
sublist <- lift . generateList $ go vmax ms
forM_ sublist $ \ es -> yield $ ei : es
lift $ writeLazy vmax ei maxcnt
可悲的是,没有人愿意为MonadPlus
定义Coroutine
,所以guard
在这里不可用。但这可能不是你想要的,因为它在一些/大多数monad中停止时会引发错误。我们当然也需要lift
在ST
monad中Coroutine
monad中完成的所有操作;小麻烦。
这就是所有代码,所以可以简单地运行它:
main :: IO ()
main =
forM_ list print
where
list = runST $ do
vmax <- thawLazy . Vector.fromList $ [1, 2, 3]
generateList (solveLP vmax [[True, True, False], [True, False, True]])
list
变量是纯粹且懒惰的。
我有点累,所以如果事情没有意义,请毫不犹豫地指出来。