О состоянии в функциональном программировании

Состояние в ФП это очень странный предмет, оно вроде есть, а вроде и нет.

Поясните, пожалуйста на простом примере. Делаем обычный кнопочный калькулятор. В процедурном программировании при нажатии цифровой клавиши изменяется переменная x, потом переменная с арифметическим оператором, потом переменная y. Мы можем добавить унарный минус, знак процента, в любой момент сменить действие перед нажатием кнопки равно. Всё это хранится в состоянии(в данном случае в переменных). При нажатии кнопки равно вызывается процедура, работающая с состоянием, которая может использовать чистые функции, но она должна передать в них состояние. Вычисленный ответ передаётся в состояние(присваивается) как новый x с которым можно продолжать вычисления. Примерно то же происходит в ООП, только вместо процедуры используется метод объекта, работающий с его состоянием. Программа может отличаться, но суть такая — процедура либо метод обрабатывают некоторое состояние.

Вопрос: как в ФП написать такой калькулятор, работающий пошагово с предыдущим вводом пользователя? Будет ли при этом храниться какое-то промежуточное состояние программы? В идеале был бы неплох функциональный пример на каком-то императивном языке, но без использования переменных, если такое возможно. Например на js или Python. Но пример на функциональных языках с объяснением тоже очень порадует.


Ответы (1 шт):

Автор решения: extrn

Состояние в функциональных языках передается в функцию в качестве дополнительного аргумента. Новое состояние возвращается функцией наряду с результатом.

Вот пример из 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

https://code.world/haskell#P46Xg_hOf3WCB_Ycx1rfF4w

→ Ссылка