Подтвердить что ты не робот

Вы находите, что вам все еще нужны переменные, которые вы можете изменить, и если да, то почему?

Один из аргументов, которые я слышал от функциональных языков, заключается в том, что кодирование с одним присваиванием слишком сложно или, по крайней мере, значительно сложнее, чем "нормальное" программирование.

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

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

values.sum

или (в случае, если сумма не указана)

function collection.sum --> inject(zero, function (v,t) --> t+v )

и

x = if a > b then a else b

или

n = case s 
  /^\d*$/ : s.to_int
  ''      : 0
  '*'     : a.length
  '?'     : a.length.random
  else    fail "I don't know how many you want"

когда вам нужно, и у вас есть список, отображение/сбор и т.д. Доступно.

Вы обнаружите, что вам все еще нужны/нужны переменные переменные в такой среде, и если да, то зачем?

Чтобы уточнить, я не прошу прочесть возражения против формы SSA, а скорее конкретные примеры, где эти возражения будут применяться. Я ищу фрагменты кода, которые являются ясными и краткими с изменяемыми переменными и не могут быть записаны без них.

Мои любимые примеры до сих пор (и лучшее возражение, которое я ожидаю от них):

  • Пол Джонсон алгоритм Фишера-Йейта, который довольно силен, когда вы включаете ограничения большого вывода. Но тогда, как указывает catulahoops, проблема с большим О не связана с вопросом SSA, а скорее с изменчивыми типами данных, и с учетом этого алгоритм может быть достаточно четко записан в SSA:

     shuffle(Lst) ->
         array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).
     shuffle(Array, 0) -> Array;
     shuffle(Array, N) ->
         K = random:uniform(N) - 1,
         Ek = array:get(K, Array),
         En = array:get(N, Array),
         shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).
    
  • jpalecek Пример области многоугольника:

    def area(figure : List[Point]) : Float = {
      if(figure.empty) return 0
      val last = figure(0)
      var first= figure(0)
      val ret = 0
      for (pt <- figure) {
        ret+=crossprod(last - first, pt - first)
        last = pt
      }
      ret
    }
    

    который все еще может быть написан как:

    def area(figure : List[Point]) : Float = {
        if figure.length < 3
            0
          else
            var a = figure(0)
            var b = figure(1)
            var c = figure(2)
            if figure.length == 3
                magnitude(crossproduct(b-a,c-a))
              else 
                foldLeft((0,a,b))(figure.rest)) { 
                   ((t,a,b),c) => (t+area([a,b,c]),a,c)
                   }
    

    Или, поскольку некоторые люди возражают против плотности этой формулировки, ее можно переделать:

    def area([])    = 0.0   # An empty figure has no area
    def area([_])   = 0.0   # ...nor does a point
    def area([_,_]) = 0.0   # ...or a line segment
    def area([a,b,c]) =     # The area of a triangle can be found directly
        magnitude(crossproduct(b-a,c-a))
    def area(figure) =      # For larger figures, reduce to triangles and sum
        as_triangles(figure).collect(area).sum
    
    def as_triangles([])      = []  # No triangles without at least three points
    def as_triangles([_])     = []
    def as_triangles([_,_])   = []
    def as_triangles([a,b,c | rest) = [[a,b,c] | as_triangles([a,c | rest])]
    
  • Принцесса указывает на трудность реализации O (1) очередей с неизменяемыми структурами, является интересной (и вполне может служить основой для убедительного примера), но, как было заявлено принципиально о изменчивости структуры данных, а не непосредственно о проблеме множественного присвоения.

  • Я заинтригован решением Сито Эратосфена, но не убежден. Правильный большой-O, вытащить столько простых чисел, сколько вы хотели бы, чтобы генератор, указанный в цитируемой им статье, выглядит нелегко для правильной реализации с SSA или без нее.


Хорошо, спасибо всем за попытку. Поскольку большинство ответов оказалось либо 1) основано на изменчивых структурах данных, но не на одиночном присвоении, и 2) в той мере, в какой они были о единичной форме назначения, легко противостоящей специалистам в данной области техники, я собираюсь удалите строку из моего разговора и/или реструктуризации (возможно, иметь ее в резервной копии в качестве темы обсуждения в маловероятном случае, когда у меня заканчиваются слова, прежде чем у меня закончится время).

Еще раз спасибо.

4b9b3361

Ответ 1

Я никогда не определял такой случай. И хотя вы всегда можете просто изобретать новые имена, как при преобразовании в форму SSA, я на самом деле считаю простым и естественным, чтобы каждое значение имело свое собственное имя. Язык, подобный Haskell, дает мне много вариантов, какие значения следует назвать, и два разных места для привязки имен (let и where). Я считаю, что форма с одним присваиванием вполне естественна и отнюдь не сложна.

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

Ответ 2

Самая сложная проблема, с которой я столкнулся, - перетасовать список. Алгоритм Fisher-Yates (также иногда известный как алгоритм Кнута) включает итерацию через список, заменяющий каждый элемент случайным другим элементом. Алгоритм O (n), хорошо известный и давно зарекомендовавший себя правильно (важное свойство в некоторых приложениях). Но для этого требуются изменяемые массивы.

Это не значит, что вы не можете перетасовать в функциональную программу. Об этом писал Олег Киселев . Но если я его правильно понимаю, функциональная перетасовка - это O (n. Log n), потому что она работает путем создания двоичного дерева.

Конечно, если мне нужно было написать алгоритм Фишера-Йейса в Haskell, я бы просто поместил его в ST monad, что позволяет вы завершаете алгоритм, включающий изменяемые массивы внутри чистой чистой функции, например:

-- | Implementation of the random swap algorithm for shuffling.  Reads a list
-- into a mutable ST array, shuffles it in place, and reads out the result
-- as a list.

module Data.Shuffle (shuffle) where


import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.STRef
import System.Random

-- | Shuffle a value based on a random seed.
shuffle :: (RandomGen g) => g -> [a] -> [a]
shuffle _ [] = []
shuffle g xs = 
    runST $ do
      sg <- newSTRef g
      let n = length xs
      v <- newListArray (1, n) xs
      mapM_ (shuffle1 sg v) [1..n]
      getElems v

-- Internal function to swap element i with a random element at or above it.
shuffle1 :: (RandomGen g) => STRef s g -> STArray s Int a -> Int -> ST s ()
shuffle1 sg v i = do
  (_, n) <- getBounds v
  r <- getRnd sg $ randomR (i, n)
  when (r /= i) $ do
    vi <- readArray v i
    vr <- readArray v r
    writeArray v i vr
    writeArray v r vi


-- Internal function for using random numbers
getRnd :: (RandomGen g) => STRef s g -> (g -> (a, g)) -> ST s a
getRnd sg f = do
  g1 <- readSTRef sg
  let (v, g2) = f g1
  writeSTRef sg g2
  return 

v

Ответ 3

Если вы хотите сделать академический аргумент, то, конечно, технически не обязательно назначать переменную более одного раза. Доказательство состоит в том, что весь код может быть представлен в форме SSA (Single Static Assignment). Действительно, это самая полезная форма для многих видов статического и динамического анализа.

В то же время есть причины, по которым мы не все пишем код в форме SSA, чтобы начать с:

  • Обычно для написания кода обычно требуется больше инструкций (или больше строк кода). Краткость имеет значение.
  • Он почти всегда менее эффективен. Да, я знаю, что вы говорите о более высоких языках - справедливое определение - но даже в мире Java и С#, вдали от сборки, скорость имеет значение. Есть несколько приложений, где скорость не имеет значения.
  • Это не так просто понять. Хотя SSA "проще" в математическом смысле, он более абстрактен из здравого смысла, что важно в реальном программировании. Если вам нужно быть очень умным, чтобы понять это, то в программировании вообще нет места.

Даже в приведенных выше примерах легко дышать. Возьмите инструкцию case. Что делать, если существует административный параметр, который определяет, разрешен ли '*', и отдельный ли для того, разрешен ли '?'? Кроме того, ноль не допускается для целочисленного случая, если у пользователя нет разрешений системы, которые его разрешают.

Это более реальный пример с ветвями и условиями. Не могли бы вы написать это как единое "выражение"? Если это так, ваше "выражение" действительно отличается от многих отдельных утверждений? Если нет, сколько временных переменных для записи вам нужно? И эта ситуация значительно лучше, чем просто наличие одной переменной?

Ответ 4

Я думаю, что вы найдете наиболее продуктивные языки, позволяющие смешивать функциональные и императивные стили, такие как OCaml и F #.

В большинстве случаев я могу написать код, который представляет собой просто длинную строку "map x to y, уменьшить y до z". В 95% случаев функциональное программирование упрощает мой код, но есть одна область, где неизменность показывает свои зубы:

Широкое несоответствие между простотой реализации и неизменяемым стеком и неизменяемой очередью.

Стеки просты и хорошо связаны с сохранением, очереди смешны.

Большинство общих реализаций неизменяемых очередей используют один или несколько внутренних стеков и стеков. Поверхность заключается в том, что эти очереди выполняются в O (1) большую часть времени, но некоторые операции будут выполняться в O (n). Если вы полагаетесь на постоянство в своем приложении, то в принципе возможно, что каждая операция выполняется в O (n). Эти очереди бесполезны, если вам нужна реальная (или, по крайней мере, непротиворечивая) производительность.

Крис Окасаки обеспечивает реализацию неизменяемых очередей в своей книге, они используют лень для достижения O (1) для всех операций. Это очень умная, разумно сжатая реализация очереди в реальном времени, но для этого требуется глубокое понимание ее основных деталей реализации, а также ее порядок на порядок более сложный, чем неизменяемый стек.

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


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

module Vector =
    type point =
        { x : float; y : float}
        with
            static member ( + ) ((p1 : point), (p2 : point)) =
                { x = p1.x + p2.x;
                  y = p1.y + p2.y;}

            static member ( * ) ((p : point), (scalar : float)) =
                { x = p.x * scalar;
                  y = p.y * scalar;}

            static member ( - ) ((p1 : point), (p2 : point)) = 
                { x = p1.x - p2.x;
                  y = p1.y - p2.y;}

    let empty = { x = 0.; y = 0.;}
    let to_tuple2 (p : point) = (p.x, p.y)
    let from_tuple2 (x, y) = { x = x; y = y;}
    let crossproduct (p1 : point) (p2 : point) =
        { x = p1.x * p2.y; y = -p1.y * p2.x }

Мы можем определить нашу функцию области, используя немного магии кортежей:

let area (figure : point list) =
    figure
    |> Seq.map to_tuple2
    |> Seq.fold
        (fun (sum, (a, b)) (c, d) -> (sum + a*d - b*c, (c, d) ) )
        (0., to_tuple2 (List.hd figure))
    |> fun (sum, _) -> abs(sum) / 2.0

Или мы можем использовать вместо этого кросс-произведение

let area2 (figure : point list) =
    figure
    |> Seq.fold
        (fun (acc, prev) cur -> (acc + (crossproduct prev cur), cur))
        (empty, List.hd figure)
    |> fun (acc, _) -> abs(acc.x + acc.y) / 2.0

Я не нахожу любую функцию нечитаемой.

Ответ 5

Этот алгоритм перетасовки тривиальен для реализации с использованием единого назначения, фактически он точно такой же, как и императивное решение с повторением итерации с хвостовой рекурсией. (Erlang, потому что я могу написать его быстрее, чем Haskell.)

 shuffle(Lst) ->
     array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).

 shuffle(Array, 0) -> Array;
 shuffle(Array, N) ->
     K = random:uniform(N) - 1,
     Ek = array:get(K, Array),
     En = array:get(N, Array),
     shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).

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

Вы не получите ответа на этот вопрос, потому что примеров не существует. Это только вопрос знакомства с этим стилем.

Ответ 6

В ответ на Джейсона -

function forbidden_input?(s)
    (s = '?' and not administration.qmark_ok) ||
    (s = '*' and not administration.stat_ok)  ||
    (s = '0' and not 'root node visible' in system.permissions_for(current_user))

n = if forbidden_input?(s)
    fail "'" + s + "' is not allowed."
  else
    case s
      /^\d*$/ : s.to_int
      ''      : 0
      '*'     : a.length
      '?'     : a.length.random
      else    fail "I don't know how many you want"

Ответ 7

Я бы пропустил задания на не чисто функциональном языке. В основном потому, что они препятствуют использованию петель. Примеры (Scala):

def quant[A](x : List[A], q : A) = {
  var tmp : A=0
  for (el <- x) { tmp+= el; if(tmp > q) return el; }
  // throw exception here, there is no prefix of the list with sum > q
}

Это должно вычислить квантиль списка, обратите внимание на накопитель tmp, который назначается несколько раз.

Аналогичным примером может быть:

def area(figure : List[Point]) : Float = {
  if(figure.empty) return 0
  val last = figure(0)
  var first= figure(0)
  val ret = 0
  for (pt <- figure) {
    ret+=crossprod(last - first, pt - first)
    last = pt
  }
  ret
}

Обратите внимание на переменную last.

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

Ответ 8

Локальные (методы) переменные, безусловно, никогда не должны назначаться дважды. Но даже в функциональном программировании допускается перераспределение переменной. Это изменение (часть) значения, которое не разрешено. И поскольку дсимча уже ответил, для очень больших структур (возможно, в корне приложения) мне не представляется возможным заменить всю структуру. Думаю об этом. Состояние приложения все включено в конечном итоге методом entrypoint вашего приложения. Если абсолютно не состояние может измениться без замены, вам придется перезапустить приложение с каждым нажатием клавиши.: (

Ответ 9

Если у вас есть функция, которая строит ленивый список/дерево, то ее снова уменьшает, функциональный компилятор может оптимизировать ее с помощью deforestation.

Если это сложно, возможно, это не так. Тогда вам не повезло, производительность и память мудры, если вы не можете перебирать и использовать изменяемую переменную.

Ответ 10

Благодаря тектору Церкви-Тьюринга мы знаем, что все, что может быть написано на полном языке Тьюринга, может быть написано на любом языке Turing. Итак, когда вы справитесь с этим, вы ничего не можете сделать в Lisp, который вы не могли бы сделать на С#, если бы вы пробовали достаточно сильно, или наоборот. (Более того, в большинстве случаев любой из них собирается скомпилировать до машинного языка x86.)

Итак, ответ на ваш вопрос: таких случаев нет. Все, что есть люди, которые легче понять в одной парадигме/языке или другой, - и легкость понимания здесь связана с обучением и опытом.

Ответ 11

Возможно, главная проблема здесь - стиль цикла в языке. В langauges, где мы используем рекурсию, любые значения, меняющиеся в течение цикла, повторно связаны, когда функция вызывается снова. Языки, использующие итераторы в блоках (например, метод Smalltalk и Ruby inject), имеют тенденцию быть похожими, хотя многие люди в Ruby все равно будут использовать each и изменяемую переменную более inject.

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

Работа в Haskell - действительно хороший способ исследовать необходимость изменяемых переменных, поскольку по умолчанию неизменяемы, но доступны переменные (как IORefs, MVars и т.д.). Я недавно, э-э, "расследую" таким образом себя и пришел к следующим выводам.

  • В подавляющем большинстве случаев изменяемые переменные не нужны, и я счастлив жить без них.

  • Для межпоточной связи изменяемые переменные являются существенными по довольно очевидным причинам. (Это специфично для Haskell, конечно, для них не нужны системы времени выполнения, которые используют передачу сообщений на самом низком уровне). Однако это использование достаточно редко, чтобы использовать функции для чтения и записи (readIORef fooRef val и т.д. ) не является большой нагрузкой.

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

Итак, в эти дни я довольно четко встал на сторону неизменяемых переменных.

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

Ответ 12

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

Ответ 13

Я не очень много думал об этом, но теперь вы указываете это.

На самом деле я стараюсь не использовать несколько назначений подсознательно.

Вот пример того, о чем я говорю, в python

start = self.offset%n
if start:
    start = n-start

Написано таким образом, чтобы избежать лишнего дополнительного модуля или вычитания. Это используется с длинными целями типа bignum, поэтому его стоит оптимизировать. Дело в том, что это действительно одно задание.

Я бы не пропустил множественное назначение вообще.

Ответ 14

Я знаю, что вы просили код, который показывал преимущества изменяемых переменных. И я бы хотел, чтобы я мог это предоставить. Но, как указывалось ранее, нет проблем, которые не могут быть выражены в обеих моделях. И тем более, что вы указали, что область jpalecek в примере с многоугольником может быть написана с помощью сгибающегося алгоритма (который является IMHO-способом messier и ставит проблему на разный уровень сложности) - хорошо, это заставило меня задуматься, почему вы опускаетесь на изменчивость, поэтому жесткий. Поэтому я попытаюсь сделать аргумент для общей основы и сосуществования неизменяемых и изменяемых данных.

По-моему, этот вопрос немного упущен. Я знаю, что мы, программисты, склонны любить вещи чистыми и простыми, но иногда мы пропустим, что смесь также возможна. И это, вероятно, почему в обсуждении вопроса о неизменности редко кто-то занимает среднюю позицию. Я просто удивляюсь, почему, потому что пусть сталкивается с этим - неизменность - отличный инструмент абстрагирования всех видов проблем. Но иногда это сильная боль в заднице. Иногда это просто слишком сдерживает. И это только заставляет меня остановиться и все - действительно ли мы хотим избавиться от изменчивости? Действительно ли это - или? Нет ли какой-либо общей земли, к которой мы можем прийти? Когда неизменяемость помогает мне быстрее достичь моих целей, когда это изменяет? Какое решение легче читать и поддерживать? (для меня это самый большой вопрос)

Многие из этих вопросов зависят от вкуса программиста и от того, для чего они используются для программирования. Поэтому я сосредоточусь на одном из аспектов, который обычно является центром большинства аргументов по непременности - Parallelism:

Часто parallelism бросается в аргумент, окружающий неизменяемость. Я работал над наборами проблем, для решения которых потребовалось 100+ процессоров в разумные сроки. И это научило мне одну очень важную вещь: большую часть времени, распараллеливая манипуляции с графиками данных, на самом деле не та вещь, которая будет наиболее эффективным способом распараллеливания. Конечно, это может принести большую пользу, но дисбаланс - настоящая проблема в этом проблемном пространстве. Поэтому, как правило, работа над несколькими изменчивыми графиками параллельно и обмен информацией с неизменяемыми сообщениями является более эффективной. Это означает, что, когда я знаю, что граф изолирован, что я не раскрыл его внешнему миру, я хотел бы выполнить свои операции над ним самым кратким образом, о котором я могу думать. И это обычно включает в себя мутирование данных. Но после этих операций над данными я хочу открыть данные по всему миру, и что точка, где я обычно немного нервничаю, если данные изменяемы. Поскольку другие части программы могут испортить данные, состояние становится недействительным... потому что после открытия в мире данные часто попадают в мир parallelism.

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

Это заставило меня прийти к простому выводу: реальная проблема для меня в том, что не из языков программирования, которые я знаю, позволяет мне сказать: "После этого момента вся эта структура данных будет неизменной" и "дайте мне изменчивую копию этой неизменной структуры данных здесь, пожалуйста, убедитесь, что только я вижу измененную копию" . Прямо сейчас я должен сам гарантировать это, перевернув битку только для чтения или что-то подобное. Если бы у нас была поддержка компилятора для него, это не только гарантировало бы мне, что я не сделал бы ничего глупого после того, как перевернул указанный бит, но это может помочь компилятору сделать различные оптимизации, которые он не мог сделать раньше. Плюс - язык по-прежнему будет привлекательным для программистов, которые более знакомы с императивной моделью программирования.

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