О состоянии в функциональном программировании
Состояние в ФП это очень странный предмет, оно вроде есть, а вроде и нет.
Поясните, пожалуйста на простом примере. Делаем обычный кнопочный калькулятор. В процедурном программировании при нажатии цифровой клавиши изменяется переменная x, потом переменная с арифметическим оператором, потом переменная y. Мы можем добавить унарный минус, знак процента, в любой момент сменить действие перед нажатием кнопки равно. Всё это хранится в состоянии(в данном случае в переменных). При нажатии кнопки равно вызывается процедура, работающая с состоянием, которая может использовать чистые функции, но она должна передать в них состояние. Вычисленный ответ передаётся в состояние(присваивается) как новый x с которым можно продолжать вычисления. Примерно то же происходит в ООП, только вместо процедуры используется метод объекта, работающий с его состоянием. Программа может отличаться, но суть такая — процедура либо метод обрабатывают некоторое состояние.
Вопрос: как в ФП написать такой калькулятор, работающий пошагово с предыдущим вводом пользователя? Будет ли при этом храниться какое-то промежуточное состояние программы? В идеале был бы неплох функциональный пример на каком-то императивном языке, но без использования переменных, если такое возможно. Например на js или Python. Но пример на функциональных языках с объяснением тоже очень порадует.
Ответы (1 шт):
Состояние в функциональных языках передается в функцию в качестве дополнительного аргумента. Новое состояние возвращается функцией наряду с результатом.
Вот пример из Haskell, где в качестве состояния выступает зерно генератора случайных чисел, а в качестве результата - пара из дробного числа и символа
import System.Random
randomPair :: StdGen -> ((Double, Char), StdGen)
randomPair gen = ((num, chr), gen'')
where
(num, gen') = randomR (5, 10) gen
(chr, gen'') = randomR ('a', 'z') gen'
ghci> randomPair $ mkStdGen 0
((7.100453103162899,'p'),StdGen {unStdGen = SMGen 14141672759607663454 16294208416658607535})
ghci> randomPair $ mkStdGen 1
((5.607634207178038,'c'),StdGen {unStdGen = SMGen 15450470250919199918 10451216379200822465})
Правда при передаче состояния вручную легко запутаться, поэтому передачу прячут за монадами и синтаксическим сахаром.
import System.Random
import Control.Monad.State
randomR' :: Random a => (a, a) -> State StdGen a
randomR' = state . randomR
randomPair' :: State StdGen (Double, Char)
randomPair' = do
num <- randomR' (5, 10)
chr <- randomR' ('a', 'z')
return (num, chr)
или даже так
randomPair' :: State StdGen (Double, Char)
randomPair' = liftM2 (,) (randomR' (5, 10)) (randomR' ('a', 'z'))
ghci> runState randomPair' $ mkStdGen 0
((7.100453103162899,'p'),StdGen {unStdGen = SMGen 14141672759607663454 16294208416658607535})
ghci> runState randomPair' $ mkStdGen 1
((5.607634207178038,'c'),StdGen {unStdGen = SMGen 15450470250919199918 10451216379200822465})
В графических приложениях за обновление состояния может отвечать функция обрабатывающая события
{-# LANGUAGE OverloadedStrings #-}
import CodeWorld
main :: IO ()
main = activityOf emptyState processEvent draw
emptyState :: [Point]
emptyState = []
processEvent :: Event -> [Point] -> [Point]
processEvent (PointerPress point) state = point : state
processEvent (KeyPress "C") state = emptyState
processEvent _ state = state
draw :: [Point] -> Picture
draw state =
pictures
[ lettering "Click to add point, press 'C' to clear"
, colored red $ foldMap drawPoint state
, colored blue $ polyline state
]
drawPoint :: Point -> Picture
drawPoint (x, y) = translated x y $ solidCircle 0.2