开发人员社区似乎正处于范式转变之中,正从面向对象编程(OOP)原则向功能编程(FP)原则转变。 我们正处于这一转变的开始。 我看到那里有很多工作职位,说他们希望有FP经验,但会接受想要学习的人。
我在本博客系列文章中的目的是通过描述我们需要适应的主要方式来解释如何考虑这种转变。 在后续文章中,我将向您介绍一些重要的FP工具,这些工具在我看来是正确的选择,例如Phoenix和Elixir作为Ruby on Rails的替代品,而Elm作为JavaScript的替代品。 尽管不是纯粹的FP工具,但还将讨论React.js,尤其是与Redux结合使用时。 放心,还有其他许多FP选项,例如Haskell,Scala和Clojure等,因此不要犹豫,也可以检查这些。
函数式编程的核心区别
在第一篇文章中,让我们进入使FP与众不同的主要因素。
- 使用功能作为其他功能(高阶功能)的输入和输出
- 使用
map
,filter
和reduce
类型函数而不是循环 - 不变的状态
- 递归代替循环
- 从其他功能组合功能
- 将“纯”功能与具有副作用的功能区分开
FP还有其他功能,例如参数多态性或monads,这些功能使我感到不那么核心,也引起了更多争议。 我会在这里坚持核心,这样我们才能弄湿自己。
用作参数
该范式之所以被称为函数编程,是因为函数可以返回函数,而函数可以使用函数。 此外,将功能链接在一起的想法允许功能组合,并且看起来功能相当糟糕。
首先,用一个简单的例子来演示其工作原理:
// 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游戏板的外观:
以下是用于生成单个合法动作的函数的两个版本:首先,一些Android版本的Java代码,然后是Elm Web版本的一些FP代码。
这不是这些代码每行需要多少行代码的比较。 他们可能会更简洁。 这也不是Java缺少FP工具的抱怨。 在开始研究FP之前以及在使用map
, filter
和reduce
之前,我编写了Java实现。 许多语言都具有FP工具(例如Lambda函数),并且Java示例当然可以以FP样式进行编码。
请注意,这些零件看起来像它们的运动能力图。 它们由代表个体能力的个体叉组成。 这些插脚可能会折断,从而导致伤口。 插脚折断时,棋子将失去该移动能力。
以下是Java代码。 如果您对Objective-C或Ruby比较满意,请参阅GitHub上的这些代码示例 。
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的语法,只是要说局部常量在顶部的let
块中定义。 在这里了解Elm代码并不重要,只是该函数返回合法移动的列表。
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
是“学习”语言中最困难的部分。 如果值可以为null或未定义,则其类型为Maybe
。 但不只是Maybe
是它本身。 请注意,下面的值是Maybe Man
。
此函数generateLegalMovesForPiece
从一个不知道正方形是否被占用的函数中调用。 广场上可能有一个人(也可能没有)。 要获得真正的男人,如果有的话,您需要一个类似于以下内容的案例陈述。 如果有一个真正的男人,那么执行路径可能Just maybeMan ->
。
请注意另一个路径_- _ ->
,这是一个通用的默认设置。 下划线是指将不使用的参数。 在这种情况下,我们返回一个空列表。 即使调用代码永远不会传递一个空的正方形,所以该分支也永远不会执行,Elm要求所有分支都必须得到寻址。 这与Elm的密封结构保持一致。
第一行(可选)声明该函数的类型签名。 第二行实际上是在声明参数时对其进行声明,以便可以对其进行引用。 还要注意lambda(匿名)函数的语法,该语法括在括号中,并以反斜杠开头,看起来像希腊的lambda字符。 ability
是输入参数,那么->
右边的代码就是实际功能。 在lambda是List参数man.abilities
,该List.map
将对其进行操作List.map
。
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#)中,您会看到类似以下的内容:
for(i = 0; i < sizeof(array); i++) {
array[i] = array[i] * 2;
}
循环变量i
是可变的。 FP倾向于完全避免这种构造。 这是Elixir网站上的FP循环构造示例:
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!
可以肯定的是,该示例遭受了示例性的困扰:它看上去不必要地复杂且冗长,但却无法说明是否有用。 我发现,执行此操作的FP方式需要我一定的信任,然后需要进行范式转换,现在感觉就像是一个功能强大的工具,可以用来表达自己想要做的事情,而无需执行一些小步骤。做吧。
但是我认为您必须先完全掌握它的功能,然后再尝试使用它。 这种做法在FP圈子中已被广泛接受,以至于许多FP语言都没有诸如while
或until
循环关键字。 收集项列表并调用对它们进行操作的函数(例如map
, filter
或reduce
)更为常见。
为了满足不变状态的要求, map
将一个列表作为输入,并转换每个项目以返回新的修改后的列表。 filter
返回一个新列表,其中的项目满足给定条件,并reduce
将列表聚合为单个返回值。
往下
请注意我的下一篇文章,其中包括:
- 使用Wounds的AI代码的不可变状态和递归的好处的更好(但更复杂)示例
- 从其他功能组合功能
- 将“纯”功能与具有副作用的功能区分开
翻译自: https://www.javacodegeeks.com/2017/09/overview-functional-programming.html