187

大多数主流语言,包括 C#、Visual Basic、C++ 和 Java 等面向对象编程 (OOP) 语言,旨在主要支持命令式(过程)编程,而类似 Haskell/gofer 的语言则是纯粹的函数式。谁能详细说明这两种编程方式有什么区别?

我知道选择编程方式取决于用户需求,但为什么建议学习函数式编程语言?

4

11 回答 11

256

这是区别:

至关重要的:

  • 开始
  • 打开你的鞋子尺寸 9 1/2。
  • 在你的口袋里腾出空间来存放一个数组[7]的钥匙。
  • 把钥匙放在房间里,把钥匙放在口袋里。
  • 进入车库。
  • 打开车库。
  • 进入汽车。

...等等等等...

  • 将牛奶放入冰箱。
  • 停止。

声明性的,其中功能性是一个子类别:

  • 牛奶是一种健康饮料,除非您在消化乳糖方面有问题。
  • 通常,人们将牛奶储存在冰箱中。
  • 冰箱是一个让里面的东西保持凉爽的盒子。
  • 商店是出售物品的地方。
  • 我们所说的“卖”是指用东西换钱。
  • 还有,用钱换东西,叫做“买”。

...等等等等...

  • 确保冰箱里有牛奶(当我们需要的时候——对于懒惰的函数式语言)。

简介:在命令式语言中,您告诉计算机如何更改其内存中的位、字节和字以及以什么顺序。在函数式中,我们告诉计算机事物、动作等是什么。例如,我们说 0 的阶乘是 1,而所有其他自然数的阶乘是该数与其前一个数的阶乘的乘积。我们不会说:要计算 n 的阶乘,保留一个内存区域并在其中存储 1,然后将该内存区域中的数字与数字 2 相乘到 n 并将结果存储在同一位置,最后,内存区域将包含阶乘。

于 2013-07-24T13:36:02.760 回答
179

定义: 命令式语言使用一系列语句来确定如何达到某个目标。据说这些语句会改变程序的状态,因为每个语句都会依次执行。

示例: Java 是一种命令式语言。例如,可以创建一个程序来添加一系列数字:

 int total = 0;
 int number1 = 5;
 int number2 = 10;
 int number3 = 15;
 total = number1 + number2 + number3; 

每个语句都会更改程序的状态,从为每个变量分配值到最终添加这些值。使用由五个语句组成的序列,程序被明确告知如何将数字 5、10 和 15 相加。

函数式语言: 函数式编程范式被明确创建以支持解决问题的纯函数式方法。函数式编程是声明式编程的一种形式。

纯函数的优点: 将函数转换实现为纯函数的主要原因是纯函数是可组合的:即自包含且无状态。这些特性带来了许多好处,包括: 提高可读性和可维护性。这是因为每个函数都旨在完成给定参数的特定任务。该函数不依赖于任何外部状态。

更容易重复开发。因为代码更容易重构,所以对设计的更改通常更容易实现。例如,假设您编写了一个复杂的转换,然后意识到某些代码在转换中重复了多次。如果你通过纯方法重构,你可以随意调用你的纯方法,不用担心副作用。

更容易测试和调试。因为纯函数可以更容易地单独测试,所以您可以编写测试代码来调用具有典型值、有效边缘情况和无效边缘情况的纯函数。

对于 OOP 人员或命令式语言:

当您对事物有一组固定的操作并且随着代码的发展而主要添加新事物时,面向对象的语言是很好的。这可以通过添加实现现有方法的新类来完成,而现有类则不理会。

当您拥有一组固定的事物并且随着代码的发展,您主要在现有事物上添加新操作时,函数式语言是很好的。这可以通过添加使用现有数据类型进行计算的新函数来完成,而现有函数则不受影响。

缺点:

编程方式的选择取决于用户的需求,所以只有用户没有选择正确的方式才有危害。

当进化走错路时,你会遇到问题:

  • 向面向对象程序添加新操作可能需要编辑许多类定义以添加新方法
  • 向函数式程序添加新事物可能需要编辑许多函数定义以添加新案例。
于 2013-07-24T06:15:04.643 回答
18

大多数现代语言在不同程度上都是命令式和函数式的,但为了更好地理解函数式编程,最好以像 Haskell 这样的纯函数式语言为例,而不是像 java/C# 这样的非函数式语言中的命令式代码。我相信通过示例总是很容易解释,所以下面是一个。

函数式编程:计算 n ie n 的阶乘!即 nx (n-1) x (n-2) x ...x 2 X 1

-- | Haskell comment goes like
-- | below 2 lines is code to calculate factorial and 3rd is it's execution  

factorial 0 = 1
factorial n = n * factorial (n - 1)
factorial 3

-- | for brevity let's call factorial as f; And x => y shows order execution left to right
-- | above executes as := f(3) as 3 x f(2) => f(2) as 2 x f(1) => f(1) as 1 x f(0) => f(0) as 1  
-- | 3 x (2 x (1 x (1)) = 6

请注意,Haskel 允许函数重载到参数值的级别。下面是命令式代码的示例,其命令性程度越来越高:

//somewhat functional way
function factorial(n) {
  if(n < 1) {
     return 1;
  }
  return n * factorial(n-1);   
}
factorial(3);

//somewhat more imperative way
function imperativeFactor(n) {
  int f = 1;
  for(int i = 1; i <= n; i++) {
     f = f * i;
  }
  return f;
}

这篇阅读文章可以很好地了解命令式代码如何更多地关注部分、机器状态(i 在 for 循环中)、执行顺序、流控制。

后面的示例可以粗略地视为 java/C# 语言代码,第一部分是语言本身的限制,而 Haskell 则按值(零)重载函数,因此可以说它不是纯粹的函数式语言,另一方面手你可以说它支持功能编。在某种程度上。

披露:上述代码均未经过测试/执行,但希望足以传达概念;我也将不胜感激任何此类更正的评论:)

于 2018-07-07T08:00:54.577 回答
12

函数式编程是声明式编程的一种形式,它描述了计算的逻辑,完全不强调执行顺序。

问题:我想把这个生物从马变成长颈鹿。

  • 拉长脖子
  • 拉长腿
  • 涂抹斑点
  • 给这个生物一个黑色的舌头
  • 去除马尾

每个项目都可以按任何顺序运行以产生相同的结果。

命令式编程是程序性的。状态和秩序很重要。

问题:我想停车。

  1. 注意车库门的初始状态
  2. 把车停在车道上
  3. 如果车库门关闭,打开车库门,记住新的状态;否则继续
  4. 把车拉进车库
  5. 关闭车库门

必须完成每个步骤才能达到预期的结果。在车库门关闭时拉入车库会导致车库门损坏。

于 2018-08-17T16:21:36.760 回答
6

函数式编程是“用函数编程”,其中函数具有一些预期的数学属性,包括引用透明性。从这些性质,进一步的性质流动,特别是通过导致数学证明的可替代性启用的熟悉的推理步骤(即证明对结果的信心)。

因此,函数式程序只是一个表达式。

通过注意命令式程序中表达式不再具有引用透明性(因此不是用函数和值构建的,并且本身不能成为函数的一部分)的地方,您可以很容易地看到两种风格之间的对比。最明显的两个地方是: 突变(例如变量) 其他副作用 非本地控制流(例如异常)

在这个由函数和值组成的程序即表达式框架上,构建了语言、概念、“功能模式”、组合子以及各种类型系统和评估算法的完整实用范式。

根据最极端的定义,几乎任何语言——即使是 C 或 Java——都可以称为函数式,但通常人们将这个术语保留给具有特定相关抽象的语言(例如闭包、不可变值和语法辅助,如模式匹配)。就函数式编程的使用而言,它涉及使用 functins 并构建没有任何副作用的代码。用来写证明

于 2017-04-26T11:45:25.103 回答
3
于 2018-10-29T22:03:52.107 回答
3

• Imperative Languages:

  • Efficient execution

  • Complex semantics

  • Complex syntax

  • Concurrency is programmer designed

  • Complex testing, has no referential transparency, has side effects

  • Has state

• Functional Languages:

  • Simple semantics

  • Simple syntax

  • Less efficient execution

  • Programs can automatically be made concurrent

  • Simple testing, has referential transparency, has no side effects

  • Has no state
于 2020-05-25T20:14:46.233 回答
1

我认为可以用命令式的方式来表达函数式编程:

  • 使用大量对象和if... else/switch语句 的状态检查
  • 一些超时/等待机制来处理异步

这种方法存在巨大的问题:

  • 重复规则/程序
  • 有状态会留下副作用/错误的机会

函数式编程,将函数/方法视为对象并拥抱无状态,是为了解决我认为的这些问题而诞生的。

使用示例:Android、iOS 等前端应用程序或网络应用程序的逻辑,包括。与后端的通信。

使用命令式/过程代码模拟函数式编程时的其他挑战:

  • 比赛条件
  • 复杂的组合和事件顺序。例如,用户尝试在银行应用程序中汇款。步骤 1) 并行执行以下所有操作,只有在一切正常时才继续 a) 检查用户是否仍然良好(欺诈、AML) b) 检查用户是否有足够的余额 c) 检查收件人是否有效且良好(欺诈、 AML) 等 步骤 2) 执行转账操作 步骤 3) 显示用户余额和/或某种跟踪的更新。以 RxJava 为例,代码简洁明了。没有它,我可以想象会有很多代码,混乱和容易出错的代码

我还相信,最终,功能代码将被编译器转换为命令式/程序化的汇编或机器代码。但是,除非您编写汇编,因为人类使用高级/人类可读语言编写代码,否则函数式编程是列出的场景更合适的表达方式

于 2018-09-17T09:20:47.620 回答
0
//The IMPERATIVE way
int a = ...
int b = ...    

int c = 0; //1. there is mutable data
c = a+b;   //2. statements (our +, our =) are used to update existing data (variable c)

An imperative program = sequence of statements that change existing data.

Focus on WHAT = our mutating data (modifiable values aka variables).

To chain imperative statements = use procedures (and/or oop).


//The FUNCTIONAL way
const int a = ... //data is always immutable
const int b = ... //data is always immutable

//1. declare pure functions; we use statements to create "new" data (the result of our +), but nothing is ever "changed"
int add(x, y) 
{
   return x+y;
} 

//2. usage = call functions to get new data
const int c = add(a,b); //c can only be assigned (=) once (const)

A functional program = a list of functions "explaining" how new data can be obtained.

Focus on HOW = our function add.

To chain functional "statements" = use function composition.


These fundamental distinctions have deep implications.

Serious software has a lot of data and a lot of code.

So same data (variable) is used in multiple parts of the code.

A. In an imperative program, the mutability of this (shared) data causes issues

  • code is hard to understand/maintain (since data can be modified in different locations/ways/moments)
  • parallelizing code is hard (only one thread can mutate a memory location at the time) which means mutating accesses to same variable have to be serialized = developer must write additional code to enforce this serialized access to shared resources, typically via locks/semaphores

As an advantage: data is really modified in place, less need to copy. (some performance gains)

B. On the other hand, functional code uses immutable data which does not have such issues. Data is readonly so there are no race conditions. Code can be easily parallelized. Results can be cached. Much easier to understand.

As a disadvantage: data is copied a lot in order to get "modifications".

See also: https://en.wikipedia.org/wiki/Referential_transparency

于 2020-12-04T23:30:11.170 回答
0

There seem to be many opinions about what functional programs and what imperative programs are.

I think functional programs can most easily be described as "lazy evaluation" oriented. Instead of having a program counter iterate through instructions, the language by design takes a recursive approach.

In a functional language, the evaluation of a function would start at the return statement and backtrack, until it eventually reaches a value. This has far reaching consequences with regards to the language syntax.

Imperative: Shipping the computer around

Below, I've tried to illustrate it by using a post office analogy. The imperative language would be mailing the computer around to different algorithms, and then have the computer returned with a result.

Functional: Shipping recipes around

The functional language would be sending recipes around, and when you need a result - the computer would start processing the recipes.

This way, you ensure that you don't waste too many CPU cycles doing work that is never used to calculate the result.

When you call a function in a functional language, the return value is a recipe that is built up of recipes which in turn is built of recipes. These recipes are actually what's known as closures.

// helper function, to illustrate the point
function unwrap(val) {
  while (typeof val === "function") val = val();
  return val;
}

function inc(val) {
  return function() { unwrap(val) + 1 };
}

function dec(val) {
  return function() { unwrap(val) - 1 };
}

function add(val1, val2) {
  return function() { unwrap(val1) + unwrap(val2) }
}

// lets "calculate" something

let thirteen = inc(inc(inc(10)))
let twentyFive = dec(add(thirteen, thirteen))

// MAGIC! The computer still has not calculated anything.
// 'thirteen' is simply a recipe that will provide us with the value 13

// lets compose a new function

let doubler = function(val) {
  return add(val, val);
}

// more modern syntax, but it's the same:
let alternativeDoubler = (val) => add(val, val)

// another function
let doublerMinusOne = (val) => dec(add(val, val));

// Will this be calculating anything?

let twentyFive = doubler(thirteen)

// no, nothing has been calculated. If we need the value, we have to unwrap it:
console.log(unwrap(thirteen)); // 26

The unwrap function will evaluate all the functions to the point of having a scalar value.

Language Design Consequences

Some nice features in imperative languages, are impossible in functional languages. For example the value++ expression, which in functional languages would be difficult to evaluate. Functional languages make constraints on how the syntax must be, because of the way they are evaluated.

On the other hand, with imperative languages can borrow great ideas from functional languages and become hybrids.

Functional languages have great difficulty with unary operators like for example ++ to increment a value. The reason for this difficulty is not obvious, unless you understand that functional languages are evaluated "in reverse".

Implementing a unary operator would have to be implemented something like this:

let value = 10;

function increment_operator(value) {
  return function() {
    unwrap(value) + 1;
  }
}

value++ // would "under the hood" become value = increment_operator(value)

Note that the unwrap function I used above, is because javascript is not a functional language, so when needed we have to manually unwrap the value.

It is now apparent that applying increment a thousand times would cause us to wrap the value with 10000 closures, which is worthless.

The more obvious approach, is to actually directly change the value in place - but voila: you have introduced modifiable values a.k.a mutable values which makes the language imperative - or actually a hybrid.

Under the hood, it boils down to two different approaches to come up with an output when provided with an input.

Below, I'll try to make an illustration of a city with the following items:

  1. The Computer
  2. Your Home
  3. The Fibonaccis

Imperative Languages

Task: Calculate the 3rd fibonacci number. Steps:

  1. Put The Computer into a box and mark it with a sticky note:

    Field Value
    Mail Address The Fibonaccis
    Return Address Your Home
    Parameters 3
    Return Value undefined

    and send off the computer.

  2. The Fibonaccis will upon receiving the box do as they always do:

    • Is the parameter < 2?

      • Yes: Change the sticky note, and return the computer to the post office:

        Field Value
        Mail Address The Fibonaccis
        Return Address Your Home
        Parameters 3
        Return Value 0 or 1 (returning the parameter)

        and return to sender.

      • Otherwise:

        1. Put a new sticky note on top of the old one:

          Field Value
          Mail Address The Fibonaccis
          Return Address Otherwise, step 2, c/oThe Fibonaccis
          Parameters 2 (passing parameter-1)
          Return Value undefined

          and send it.

        2. Take off the returned sticky note. Put a new sticky note on top of the initial one and send The Computer again:

          Field Value
          Mail Address The Fibonaccis
          Return Address Otherwise, done, c/o The Fibonaccis
          Parameters 2 (passing parameter-2)
          Return Value undefined
        3. By now, we should have the initial sticky note from the requester, and two used sticky notes, each having their Return Value field filled. We summarize the return values and put it in the Return Value field of the final sticky note.

          Field Value
          Mail Address The Fibonaccis
          Return Address Your Home
          Parameters 3
          Return Value 2 (returnValue1 + returnValue2)

          and return to sender.

As you can imagine, quite a lot of work starts immediately after you send your computer off to the functions you call.

The entire programming logic is recursive, but in truth the algorithm happens sequentially as the computer moves from algorithm to algorithm with the help of a stack of sticky notes.

Functional Languages

Task: Calculate the 3rd fibonacci number. Steps:

  1. Write the following down on a sticky note:

    Field Value
    Instructions The Fibonaccis
    Parameters 3

That's essentially it. That sticky note now represents the computation result of fib(3).

We have attached the parameter 3 to the recipe named The Fibonaccis. The computer does not have to perform any calculations, unless somebody needs the scalar value.

Functional Javascript Example

I've been working on designing a programming language named Charm, and this is how fibonacci would look in that language.

fib: (n) => if (                         
  n < 2               // test
  n                   // when true
  fib(n-1) + fib(n-2) // when false
)
print(fib(4));

This code can be compiled both into imperative and functional "bytecode".

The imperative javascript version would be:

let fib = (n) => 
  n < 2 ?
  n : 
  fib(n-1) + fib(n-2);

The HALF functional javascript version would be:

let fib = (n) => () =>
  n < 2 ?
  n :
  fib(n-1) + fib(n-2);

The PURE functional javascript version would be much more involved, because javascript doesn't have functional equivalents.

let unwrap = ($) =>
  typeof $ !== "function" ? $ : unwrap($());

let $if = ($test, $whenTrue, $whenFalse) => () =>
  unwrap($test) ? $whenTrue : $whenFalse;

let $lessThen = (a, b) => () =>
  unwrap(a) < unwrap(b);

let $add = ($value, $amount) => () =>
  unwrap($value) + unwrap($amount);

let $sub = ($value, $amount) => () =>
  unwrap($value) - unwrap($amount);

let $fib = ($n) => () =>
  $if(
    $lessThen($n, 2),
    $n,
    $add( $fib( $sub($n, 1) ), $fib( $sub($n, 2) ) )
  );

I'll manually "compile" it into javascript code:

"use strict";

// Library of functions:
  /**
   * Function that resolves the output of a function.
   */
  let $$ = (val) => {
    while (typeof val === "function") {
      val = val();
    }
    return val;
  }

  /**
   * Functional if
   *
   * The $ suffix is a convention I use to show that it is "functional"
   * style, and I need to use $$() to "unwrap" the value when I need it.
   */
  let if$ = (test, whenTrue, otherwise) => () =>
    $$(test) ? whenTrue : otherwise;

  /**
   * Functional lt (less then)
   */
  let lt$ = (leftSide, rightSide)   => () => 
    $$(leftSide) < $$(rightSide)


  /**
   * Functional add (+)
   */
  let add$ = (leftSide, rightSide) => () => 
    $$(leftSide) + $$(rightSide)

// My hand compiled Charm script:

  /**
   * Functional fib compiled
   */
  let fib$ = (n) => if$(                 // fib: (n) => if(
    lt$(n, 2),                           //   n < 2
    () => n,                             //   n
    () => add$(fib$(n-2), fib$(n-1))     //   fib(n-1) + fib(n-2)
  )                                      // )

// This takes a microsecond or so, because nothing is calculated
console.log(fib$(30));

// When you need the value, just unwrap it with $$( fib$(30) )
console.log( $$( fib$(5) ))

// The only problem that makes this not truly functional, is that
console.log(fib$(5) === fib$(5)) // is false, while it should be true
// but that should be solveable

https://jsfiddle.net/819Lgwtz/42/

于 2021-01-26T18:19:09.543 回答
-1

我知道这个问题比较老,其他人已经很好地解释了,我想举一个简单的例子来解释相同的问题。

问题:写 1 的表。

解决方案: -

按命令式:=>

    1*1=1
    1*2=2
    1*3=3
    .
    .
    .
    1*n=n 

按功能风格:=>

    1
    2
    3
    .
    .
    .
    n

命令式的解释我们更明确地编写指令,并且可以以更简化的方式调用。

在功能风格中,不言自明的东西将被忽略。

于 2018-05-03T07:33:49.970 回答