Статьи

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

Сообщество разработчиков, похоже, находится в процессе смены парадигмы, переходя от принципов объектно-ориентированного программирования (ООП) к принципам функционального программирования (ФП). Мы в начале этого сдвига. Я видел много объявлений о работе, где говорилось, что они хотели бы получить опыт работы с FP, но примут кого-то, кто хочет учиться.

Мое намерение в этой серии блогов состоит в том, чтобы объяснить, как думать об этом сдвиге, описав основные способы, которые нам понадобятся для адаптации. В последующих статьях я познакомлю вас с несколькими важными инструментами FP, которые, на мой взгляд, являются правильными лошадками для ставок, а именно с Phoenix и Elixir в качестве альтернативы Ruby on Rails и Elm в качестве альтернативы JavaScript. React.js, хотя и не является чистым инструментом FP, особенно когда он используется в сочетании с Redux, также будет обсуждаться. Будьте уверены, есть много других опций FP, таких как Haskell, Scala и Clojure, чтобы назвать несколько, так что не стесняйтесь проверить их тоже.

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

Для этого первого поста давайте разберемся с основными вещами, которые выделяют FP.

  • Использование функций в качестве входа и выхода из других функций, функций высшего порядка
  • Использование функций map , filter и reduce типов вместо зацикливания
  • Неизменное состояние
  • Рекурсия вместо зацикливания
  • Составление функций из других функций
  • Отличие «чистых» функций от функций с побочными эффектами

Есть и другие особенности FP, такие как параметрический полиморфизм или монады, которые кажутся мне менее важными и вызывают больше споров. Я буду придерживаться основной темы, чтобы мы могли намочить ноги.

Функции как параметры

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

Во-первых, тривиальный пример, демонстрирующий, как это работает:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// return a function which can be used to build other functions
function addValue(valueToAdd) {
    return function(valueThatGetsAddedTo) {
        return valueToAdd + valueThatGetsAddedTo;
    };
}
 
function add2() {
    return addValue(2);
}
 
function add3() {
    return addValue(3);
}
 
add2(7) // returns 9
 
add3(7) // returns 10

Функция addValue возвращает другую функцию, для которой будет добавлено жестко закодированное число. Он используется для создания функций add2 и add3 , которые выполняют фактическую арифметику. Сейчас я приведу сложный пример, чтобы вы могли понять, почему это мощно.

У меня есть любимый проект, который я реализовал в Ruby, Objective-C, Java, Swift и совсем недавно в Elm. В своей карьере консультанта мне часто приходилось подбирать новые языки и среды разработки, и мне каждый раз было удобно заново реализовывать свой любимый проект. Таким образом, я могу сосредоточиться на выполнении нетривиальных задач в новой среде, но мне не нужно тратить время на разработку.

Мой любимый проект называется Раны. По сути, это игра в шахматы с добавлением способности к ранениям, а не к захвату. Я буду использовать этот проект в последующих постах этого цикла, чтобы продемонстрировать концепции в Elm и React / Redux / React Native.

Вот как выглядит игровое поле Wounds:

Ниже приведены две версии функции для генерации допустимых ходов для одного куска: сначала немного кода Java для версии Android, а затем немного кода FP в Elm для веб-версии.

Это не сравнение количества строк кода, которое занимает каждая из этих строк. Они могут быть немного более краткими. Также это не претензия к отсутствию инструментов FP в Java. Я написал Java-реализацию до того, как начал изучать FP, а также перед тем, как поиграть с map , filter и reduce . Во многих языках есть инструменты FP, такие как функции Lambda, и пример Java, безусловно, можно кодировать больше в стиле FP.

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

Ниже приведен код Java. Если вам удобнее использовать Objective-C или Ruby, посмотрите эти примеры кода на GitHub .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public ArrayList<Move> generateLegalMoves(int x, int y, Board
                        board, Player player, int fromIndex)
{
  ArrayList<Move> generatedMoves = new ArrayList<Move>();
  if(board.squares.get(fromIndex) != null)
  {
      Man man = board.squares.get(fromIndex);
      if(man.player == player)
      {
          for(Ability ability:man.abilities)
          {
              int dest_x = x + ability.delta_x;
              int dest_y = y + ability.delta_y;
              int dest_index =
            board.indexFromXY(dest_x, dest_y);
              if(!ability.slide) // slide is like a bishop, rook, or queen
              {
                  if(board.isOnBoardXY(dest_x, dest_y))
                  {
 
                      Move move = new Move();
                      move.attacking_man = man;
                      move.from_x = x;
                      move.from_y = y;
                      move.to_x = dest_x;
                      move.to_y = dest_y;
                       
                      boolean addThisMove = false;
                      if(board.squares.get(dest_index) != null)
                      {
                          addThisMove = true;
                      }
                      else
                      {
                          if(board.friendlyPiece(dest_index, player))
                          {
                              addThisMove = true;
                              move.defending_man =
                        board.squares.get(dest_index);
                          }
                      }
                      if(addThisMove)
                      {
                          generatedMoves.add(move);
                      }
                  }
              }
              else // it’s a slide, like a bishop, rook, or queen
              {
                  boolean ranIntoAnEnemy = false;
                  while(board.isOnBoardXY(dest_x, dest_y) &&
            !board.friendlyPiece(dest_index, player)
                && !ranIntoAnEnemy)
                  {                        
                      Move slideMove = new Move();
                      slideMove.attacking_man = man;
                      slideMove.from_x = x;
                      slideMove.from_y = y;
                      slideMove.to_x = dest_x;
                      slideMove.to_y = dest_y;
                      if(board.enemyPiece(dest_index, player))
                      {
                          ranIntoAnEnemy = true;
                          slideMove.defending_man =
                    board.squares.get(dest_index);
                      }
                      generatedMoves.add(slideMove);
                      dest_x += ability.delta_x;
                      dest_y += ability.delta_y;
                      dest_index = board.indexFromXY(dest_x, dest_y);
                  }
              }
          }
      }
  }
  return generatedMoves;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
addMoveToList : Ability -> Board -> Int -> Man -> List Move -> List Move
addMoveToList ability board index man moveList =
  let
    file = (rem index board.width)
    rank = (index // board.width)
    toFile = file + ability.xOffset
    toRank = rank + ability.yOffset
    defendingMan = getMan board toFile toRank
    defendingAbility = Man.getDefendingAbility defendingMan ability
    nextIndex = (toRank * board.width) + toFile
    isLegalMove board toFile toRank man defendingMan =
      (toFile >= 0) && (toFile < board.width) && (toRank >= 0)
    && (toRank < board.height) && not (sameTeam defendingMan man)
  in
    if isLegalMove board toFile toRank man defendingMan then
      if (ability.abilityType == Slide) && (defendingMan == Nothing) then
        moveList ++ [Move file rank toFile toRank man defendingMan
        ability defendingAbility]
        ++ addMoveToList ability board nextIndex man moveList
      else
        moveList ++ [Move file rank toFile toRank man defendingMan
        ability defendingAbility]
    else
      moveList — move was illegal, so return the list unchanged

У Вяза есть концепция « Maybe , которая является «возможно» самой сложной частью изучения языка. Если значение может быть нулевым или неопределенным, оно имеет тип Maybe . Но не просто Maybe само собой. Обратите внимание, что у нас есть значение Maybe Man .

Эта функция, generateLegalMovesForPiece , generateLegalMovesForPiece из функции, которая не знает, занят ли квадрат. На площади может быть человек (или нет). Чтобы получить настоящего мужчину, если таковой имеется, вам нужно заявление о ситуации, подобное приведенному ниже. Если есть настоящий мужчина, дело « Just maybeMan -> будет путем выполнения.

Обратите внимание на другой путь, _ -> , который является стандартным по умолчанию. Подчеркивание относится к параметру, который не будет использоваться. В этой ситуации мы возвращаем пустой список. Хотя вызывающий код никогда не пройдет пустой квадрат, поэтому эта ветвь никогда не будет выполнена, Elm требует, чтобы все ветви были адресованы. Это соответствует герметичной архитектуре Вяза.

Первая (необязательная) строка объявляет сигнатуры типов для функции. Вторая строка фактически объявляет это, называя параметры, чтобы на них можно было ссылаться. Также обратите внимание на синтаксис лямбда-функции (анонимной), заключенной в скобки и начинающейся с обратной косой черты, которая должна выглядеть как греческий лямбда-символ. ability является входным параметром, тогда код справа от -> является фактической функцией. После того, как лямбда является параметром List, man.abilities , с которым будет работать List.map .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
generateLegalMovesForPiece : Board -> Maybe Man -> Int -> List Move
generateLegalMovesForPiece board maybeMan index =
  case maybeMan of
    Just maybeMan ->
      let
        man = maybeMan
        moveList = []
        moveListList = List.map
        (\ability -> addMoveToList ability board index man moveList)
        man.abilities
      in
        List.concat moveListList
   _ ->
      []

Мне пришлось сыграть в несколько игр, которые я позже реорганизую. Функция addMoveToList должна иметь согласованное возвращаемое значение. Поэтому у меня не может быть функции, которая возвращает ход, если ход законный, и ничего, если это не так. Поэтому я использую клюге. Я передаю пустой список в функцию, а затем возвращаю этот список либо пустым, либо с разрешенным ходом. Затем я использую функцию List.concat moveListList для объединения результирующего списка списков.

Это не хорошее функциональное программирование.

Суть в том, что вместо того, чтобы иметь циклическую конструкцию, которая циклически перебирает способности человека, у нас есть вызов List.map , который работает с каждой способностью в списке. Обратите внимание, что входной список представляет собой список ability типа, а выходной список имеет тип move .

Зарегистрируй бесплатный аккаунт Codeship

Неизменное состояние

Типичным примером состояния может быть то, вошел ли пользователь в систему или нет. Я привык думать, что у меня где-то есть переменная, которая содержит это состояние. Когда пользователь входит в систему, я устанавливаю значение true. Когда они снова выходят из системы, я установил для него значение false.

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

Без петель (как таковых)

В языках стиля C (C, C ++, Java, JavaScript, C #) вы видите такие вещи:

1
2
3
for(i = 0; i < sizeof(array); i++) {
  array[i] = array[i] * 2;
}

Циклическая переменная i является изменяемой. FP имеет тенденцию вообще избегать этой конструкции. Вот пример конструкции цикла FP с сайта Elixir:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
defmodule Recursion do
  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end
 
  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end
end
 
Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!

По общему признанию, этот пример страдает от example-itis: он выглядит излишне сложным и многословным, не приводя доводов в пользу полезного метода. То, что я обнаружил, это то, что способ FP сделать это потребовал некоторого доверия с моей стороны, а затем смены парадигмы, и теперь это похоже на мощный инструмент, где я могу выразить то, что я хочу сделать, вместо маленьких шагов, необходимых для сделай это.

Но я думаю, что вы должны поиграть с ним, прежде чем полностью понять его силу. Эта практика настолько хорошо принята в кругах FP, что многие языки FP не имеют ключевых слов для циклов, таких как while или until . Чаще всего собирают список элементов и вызывают функцию, которая работает с каждым из них, например map , filter или reduce .

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

Следующий

Будьте в поисках моего следующего поста, который будет включать в себя:

  • Лучшие (но более сложные) примеры преимуществ неизменяемого состояния и рекурсии с использованием кода AI из Wounds
  • Составление функций из других функций
  • Отличие «чистых» функций от функций с побочными эффектами
Ссылка: Обзор функционального программирования от нашего партнера по JCG Эрика Форда в блоге Codeship Blog .