Главный вопрос, с которым я хотел бы помочь: есть ли идиоматический способ написания на Haskell такой рекурсии монадических значений, приводящий к монадическому списку, который похож на структуру чистой функции?
Вы можете сделать это в два этапа. Пусть ваша рекурсивная функция вернет список «монадических действий», а затем скомпонует / упорядочит эти действия.
Давайте рассмотрим более простую, но аналогичную функцию для удобства изложения. Вместо случайности давайте рассмотрим ввод. Список, к которому вы обращаетесь, предназначен только для размера (содержимое игнорируется), поэтому давайте просто используем целое число.
rc :: Int -> [Double] -> IO [Double]
rc 0 h = return h
rc n h@(cr:_) = do rand <- readLn :: IO Double
let nx = cr + rand
rc (n-1)(nx:h)
Вот аналогичная альтернатива, которая работает так, как вы хотите
rc' :: Int -> Double -> IO [Double]
rc' 0 cr = return []
rc' n cr = do rand <- readLn :: IO Double
let nx = cr + rand
xs <- rc' (n-1) nx
return (nx : xs)
А здесь без обозначений
rc'' :: Int -> Double -> IO [Double]
rc'' 0 cr = return []
rc'' n cr = (readLn :: IO Double) >>= (\rand ->
let nx = cr + rand
in (rc'' (n-1) nx) >>= (\xs ->
return (nx : xs)))
В любом случае, еще одна вещь, которую вы можете сделать, это абстрагировать отрывки кода, а не иметь монолитное представление.
На каждом шаге вам требуется текущее значение для генерации нового. Таким образом, шаг является функцией типа Double -> IO Double
. И это довольно аккуратный тип, фундаментальный в мире монад. Вы можете привязать значения к шагу с помощью x >>= step
или составить два шага с помощью step1 >=> step2
. Итак, давайте пойдем с этим.
step :: Double -> IO Double
step cr = do rand <- readLn :: IO Double
return (cr + rand)
Это очень легко понять. Вы «генерируете» число, добавляете текущий и возвращаете результат. И вы хотите сделать n
таких шагов, поэтому составьте список шагов.
steps :: Int -> [Double -> IO Double]
steps n = replicate n step
Теперь вы можете выбирать, как их комбинировать. Например, было бы очень естественно сложить список шагов с помощью >=>
. Вы получите это,
runSteps :: Int -> Double -> IO Double
runSteps n = foldr (>=>) return (steps n)
Это близко к тому, что вы хотите, но возвращает только конечный результат, а не накапливать сгенерированные значения на каждом шаге. Ниже приведен (ограниченный) тип (>=>)
и тип оператора (*=>)
, который мы хотим.
(>=>) :: Monad m => (a -> m a) -> (b -> m a) -> a -> m a
(*=>) :: Monad m => (a -> m a) -> (a -> m [a]) -> a -> m [a]
Определение:
(*=>) :: Monad m => (a -> m a) -> (a -> m [a]) -> a -> m [a]
(*=>) ac uc c = do x <- ac c
xs <- uc x
return (x:xs)
Я действительно думаю, что это заключает в себе тот бит, который вам не особенно понравился. Теперь мы абстрагировали его от этого изолированного фрагмента кода. Даже подальше от рекурсивных звонков. И, наконец, мы просто складываемся, чтобы выполнить шаги.
execSteps :: Int -> Double -> IO [Double]
execSteps n = foldr (*=>) (\x -> return []) (steps n)
Эта функция отличается от оригинальной тем, что в качестве исходного ввода используется Double
, а не [Double]
. Но это тот тип, который имеет смысл. Вы бы просто передали один обернутый двойник в исходной функции. И он накапливает элементы в «правильном» порядке, как вы просили.