Считаете ли вы, что вам все еще нужны переменные, которые вы можете изменить, и если да, то почему? - PullRequest
33 голосов
/ 04 марта 2009

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

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

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

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, а скорее конкретные примеры, где эти возражения будут применяться. Я ищу фрагменты кода, которые ясны и лаконичны с изменяемыми переменными и не могут быть написаны без них.

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

  1. Ответ Пола Джонсона Алгоритма Фишера-Йейтса , который довольно силен, когда вы включаете ограничения big-O. Но затем, как указывает catulahoops, проблема большого O связана не с вопросом о 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).
    
  2. jpalecek's площадь многоугольника пример:

    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])]
    
  3. Идея принцессы о сложности реализации O (1) очередей с неизменяемыми структурами интересна (и вполне может послужить основой для убедительного примера), но, как уже говорилось, она в основном касается изменчивости структуры данных, а не непосредственно о проблеме множественного назначения.

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


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

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

Ответы [ 14 ]

16 голосов
/ 07 марта 2009

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

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

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

-- | 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

15 голосов
/ 04 марта 2009

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

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

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

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

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

11 голосов
/ 05 марта 2009

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

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

6 голосов
/ 10 марта 2009

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

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

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

Стеки легки и хорошо сочетаются с настойчивостью, очереди смешны.

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

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

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


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

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

Я не нахожу ни одну из функций нечитабельными.

4 голосов
/ 08 марта 2009

Этот алгоритм тасования тривиален для реализации с использованием одного присваивания, фактически он точно такой же, как императивное решение с итерацией, переписанной в хвостовую рекурсию (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).

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

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

3 голосов
/ 07 марта 2009

Я бы пропустил задания на не чисто функциональном языке. Главным образом потому, что они препятствуют полезности петель. Примеры (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.

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

3 голосов
/ 04 марта 2009

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

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"
1 голос
/ 18 мая 2009

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

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

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

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

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

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

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

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

1 голос
/ 09 марта 2009

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

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

1 голос
/ 07 марта 2009

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

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

...