Является ли изменение переменной объекта за пределами объекта побочным эффектом?

Я пытаюсь понять функциональное и объектно-ориентированное программирование. Что я сейчас пытаюсь понять, так это понятие побочных эффектов в объектно-ориентированном программировании, особенно в том, что касается повышения безопасности программы.

Я понимаю побочный эффект как все, что изменяет переменные, связанные с одной функцией внутри объекта, которые используются другой функцией, используемой объектом. Но как насчет того, чтобы разрешить установку объектных переменных вне объекта, но не внутри функции внутри объекта.

Мне кажется, что это было бы безопаснее, чем установить его в другой функции. И функции внутри этого объекта, которые используют эту переменную, будут знать, что ни одна другая функция не изменит ее без предварительного уведомления.

Я что-то упускаю? Вы все еще считаете это побочным эффектом? Как насчет установки множества переменных при инициализации объекта?


person mczarnek    schedule 03.09.2020    source источник
comment
все, что делает функция, оказывающая заметное влияние на что-либо еще в программе или во внешнем мире, считается побочным эффектом. Единственная полезная вещь, которую функции могут делать помимо побочных эффектов, — это принимать аргументы и возвращать результат на их основе. Другими словами, какой-то расчет. И хотя это кажется действительно ограничивающим, если вы не привыкли к функциональному программированию, такой язык, как Haskell, доказывает, что вы действительно можете написать большинство программ с чистыми функциями и ограничить побочные эффекты лишь несколькими местами, где они абсолютно необходимы.   -  person Robin Zigmond    schedule 03.09.2020
comment
Так что да, изменение значения переменной в функции является побочным эффектом, по крайней мере, когда эта переменная может быть прочитана извне функции - как переменные экземпляра в ООП. Haskell обходит это, практически не имея изменяемых переменных, так что вам не о чем беспокоиться.   -  person Robin Zigmond    schedule 03.09.2020
comment
@RobinZigmond Где есть места, где побочные эффекты абсолютно необходимы?   -  person mczarnek    schedule 05.09.2020
comment
Места, где он не может избежать взаимодействия с внешним миром. Например, вывод текста/графики на экран, взаимодействие с базой данных или файлами, выполнение сетевых запросов и т. д.   -  person Robin Zigmond    schedule 05.09.2020


Ответы (2)


Побочный эффект — это все, что позволяет вам наблюдать разницу в поведении программы в зависимости от того, было ли, сколько раз или в каком порядке оценивается выражение или выполняется действие, то есть нарушается прозрачность ссылок. Изменение переменных является одним из примеров побочного эффекта, как и отправка сообщения по параллельному каналу, печать на терминал, запись в файл или чтение из сети.

Именно наблюдаемость внутри обычного безопасного кода делает что-то побочным; Среда выполнения Haskell все время использует изменяемые переменные для ленивых вычислений, но вы не можете увидеть это изнутри языка без небезопасного кода. Если возможно наблюдать эффект относительно контекста, в котором вы находитесь, это все равно побочный эффект. Итак, то, что вы описываете (ограничение тех, кто может изменять поля объекта), звучит, может быть, безопаснее, но оно не лишено побочных эффектов.

Например, Debug.Trace.trace :: String -> a -> a имеет побочный эффект при оценке, потому что trace "x" (1 :: Int) + trace "x" (1 :: Int) заметно отличается от let x = trace "x" (1 :: Int) in x + x:

> trace "x" (1 :: Int) + trace "x" (1 :: Int)
x
x
2

> let x = trace "x" (1 :: Int) in x + x
x
2

modifyIORef :: IORef a -> (a -> a) -> IO () имеет побочный эффект при выполнении, поскольку многократное изменение изменяемой ссылки явно отличается от ее однократного изменения:

increment :: IORef Int -> IO ()
increment r = modifyIORef r (+ 1)

main :: IO ()
main = do

  r1 <- newIORef 0
  increment r1
  print =<< readIORef r1  -- 1

  r2 <- newIORef 0
  increment r2
  increment r2
  print =<< readIORef r2  -- 2

(Но обратите внимание, что значение типа IO a для некоторых a является чистым при вычислении: это не значение типа a, «помеченное» тегом тот факт, что это произошло из ввода-вывода; скорее, это программа или действие, которое возвращает значение типа a при подключении к main и выполняется средой выполнения.)

Обратите внимание, что не весь эффективный код является побочным: pure () :: IO () находится в IO, но явно не имеет побочных эффектов. Аналогично, ST предоставляет локальные изменяемые переменные, которые гарантированно не исчезнут и не будут видны за пределами своей области видимости, поэтому вы можете реализовать чистую функцию, которая нечиста внутри:

pureSum :: Int -> Int
pureSum n = sum [1 .. n]

impureSum :: Int -> IO Int
impureSum n = do
  result <- newIORef 0
  for_ [1 .. n] $ \ x -> do
    putStrLn ("Adding " ++ show x)  -- Side effect!
    modifyIORef result (+ x)
  readIORef result

internallyImpureSum :: Int -> Int
internallyImpureSum = runST $ do
  result <- newSTRef 0

  for_ [1 .. n] $ \ x -> do
    -- Can’t perform any side effects observable outside.
    modifySTRef result (+ x)

  -- Can *read* the reference, but returning
  -- the reference ‘result’ itself would be
  -- a type error.
  readSTRef result

Что касается «установки набора переменных при инициализации объекта», то в основном это шаблон, используемый в Haskell не только для обеспечения безопасности, но и как основанная на математике философия моделирования данных в целом.

В языках ООП соглашение о моделировании изменяющегося состояния заключается в создании единого объекта, который имеет понятие идентичности, и изменении его с течением времени с помощью команд или прямого изменения. Ожидается, что объект останется действительным, поддерживая все свои инварианты при каждом изменении состояния.

В то время как в Haskell принято, что объект является неизменяемым снимком или представлением состояния, и вы можете смоделировать изменяющееся состояние, просто создав новое значение для представления новое состояние. Если вам больше не нужен старый, просто забудьте о нем и дайте ему быть собранным мусором. Объекту не нужно поддерживать какие-либо инварианты после создания, потому что он неизменяем: ему просто нужно принудительно применять инварианты один раз при создании. Это можно сделать с помощью точного моделирования данных с помощью алгебраических типов данных (т. н. «сделать недопустимые состояния непредставимыми») или с помощью функций инкапсуляции и интеллектуального конструктора для предотвращения создания недопустимых значений (т. н. «правильность путем построения»).

person Jon Purdy    schedule 03.09.2020
comment
Я не думаю, что один раз против нескольких — хорошее определение побочного эффекта. Идемпотентный эффект не будет считаться эффектом в соответствии с этим определением. - person Fyodor Soikin; 03.09.2020
comment
@FyodorSoikin: Хороший вопрос. Думаю, я имел в виду «ноль, один или много». Я немного перефразировал, но трудно найти хорошее содержательное объяснение того, что я имею в виду. По сути, я думаю, что «побочные эффекты» всегда зависят от контекста наблюдения. В чистом State/ST действии modify/modifySTRef является SE внутри общего runState/runST (поскольку все дело в том, что последующие действия могут видеть изменение), а не снаружи этот контекст; аналогично, putStrLn "hi" является SE внутри IO, но чисто от точки зрения безопасного Haskell как чисто функционального метаязыка для построения IO программ. - person Jon Purdy; 04.09.2020
comment
Если вы работаете в нечистой среде, первое, что вы заметите, — во многих случаях потеря идемпотентности. Я думаю, что этот аргумент по-прежнему законен, даже если он не верен для всех классов побочных эффектов. Другое менее техническое объяснение состоит в том, что побочные эффекты вводят время. Я могу ошибаться в этом, но я думаю, что ассоциативный закон является наиболее важным, который обеспечивает отсутствие времени в FP. Таким образом, побочные эффекты мешают ассоциативности. - person Iven Marquardt; 04.09.2020

Побочным эффектом может быть любое изменение наблюдаемого состояния; внутри и вне функции не является полезным или четко определенным квалификатором для этой цели.

Рассмотрим локальную переменную static в C, которая создает глобальную переменную, доступную для этой функции. Проблема в том, что функцию можно вызывать из любого места, в любом потоке. Неважно, рассматриваете ли вы переменную внутри функции или нет: если функция может читать и обновлять статическую переменную, она не является реентерабельной из-за побочного эффекта.

Опасность побочного эффекта в том, что он скрыт от программиста. Если наблюдаемое состояние понятно программисту, то можно утверждать, что это просто эффект, а не побочный эффект. Например, ожидается, что неконстантный метод C++ сможет обновлять состояние своего объекта. Каждый раз, когда вы вызываете метод, вам нужно предоставить целевой объект для его обновления; этот вид эффекта не так опасен, как статическая переменная, потому что наблюдаемое состояние очевидно (а не в стороне). Однако у вас все еще могут возникнуть проблемы из-за псевдонимов: например, если один из параметров метода оказывается другой ссылкой на целевой объект...

В некоторой степени побочный эффект также связан с тем, что вы решили считать важным. Например, вы можете запустить чистую функцию в отладчике и установить точку останова. Является ли функция менее чистой, потому что она может печатать трассировку стека на вашем экране? Это зависит от того, считаете ли вы это важным (в данном случае, вероятно, нет).

person comingstorm    schedule 03.09.2020