大多数主流语言,包括 C#、Visual Basic、C++ 和 Java 等面向对象编程 (OOP) 语言,旨在主要支持命令式(过程)编程,而类似 Haskell/gofer 的语言则是纯粹的函数式。谁能详细说明这两种编程方式有什么区别?
我知道选择编程方式取决于用户需求,但为什么建议学习函数式编程语言?
大多数主流语言,包括 C#、Visual Basic、C++ 和 Java 等面向对象编程 (OOP) 语言,旨在主要支持命令式(过程)编程,而类似 Haskell/gofer 的语言则是纯粹的函数式。谁能详细说明这两种编程方式有什么区别?
我知道选择编程方式取决于用户需求,但为什么建议学习函数式编程语言?
这是区别:
至关重要的:
...等等等等...
声明性的,其中功能性是一个子类别:
...等等等等...
简介:在命令式语言中,您告诉计算机如何更改其内存中的位、字节和字以及以什么顺序。在函数式中,我们告诉计算机事物、动作等是什么。例如,我们说 0 的阶乘是 1,而所有其他自然数的阶乘是该数与其前一个数的阶乘的乘积。我们不会说:要计算 n 的阶乘,保留一个内存区域并在其中存储 1,然后将该内存区域中的数字与数字 2 相乘到 n 并将结果存储在同一位置,最后,内存区域将包含阶乘。
定义: 命令式语言使用一系列语句来确定如何达到某个目标。据说这些语句会改变程序的状态,因为每个语句都会依次执行。
示例: Java 是一种命令式语言。例如,可以创建一个程序来添加一系列数字:
int total = 0;
int number1 = 5;
int number2 = 10;
int number3 = 15;
total = number1 + number2 + number3;
每个语句都会更改程序的状态,从为每个变量分配值到最终添加这些值。使用由五个语句组成的序列,程序被明确告知如何将数字 5、10 和 15 相加。
函数式语言: 函数式编程范式被明确创建以支持解决问题的纯函数式方法。函数式编程是声明式编程的一种形式。
纯函数的优点: 将函数转换实现为纯函数的主要原因是纯函数是可组合的:即自包含且无状态。这些特性带来了许多好处,包括: 提高可读性和可维护性。这是因为每个函数都旨在完成给定参数的特定任务。该函数不依赖于任何外部状态。
更容易重复开发。因为代码更容易重构,所以对设计的更改通常更容易实现。例如,假设您编写了一个复杂的转换,然后意识到某些代码在转换中重复了多次。如果你通过纯方法重构,你可以随意调用你的纯方法,不用担心副作用。
更容易测试和调试。因为纯函数可以更容易地单独测试,所以您可以编写测试代码来调用具有典型值、有效边缘情况和无效边缘情况的纯函数。
对于 OOP 人员或命令式语言:
当您对事物有一组固定的操作并且随着代码的发展而主要添加新事物时,面向对象的语言是很好的。这可以通过添加实现现有方法的新类来完成,而现有类则不理会。
当您拥有一组固定的事物并且随着代码的发展,您主要在现有事物上添加新操作时,函数式语言是很好的。这可以通过添加使用现有数据类型进行计算的新函数来完成,而现有函数则不受影响。
缺点:
编程方式的选择取决于用户的需求,所以只有用户没有选择正确的方式才有危害。
当进化走错路时,你会遇到问题:
大多数现代语言在不同程度上都是命令式和函数式的,但为了更好地理解函数式编程,最好以像 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 则按值(零)重载函数,因此可以说它不是纯粹的函数式语言,另一方面手你可以说它支持功能编。在某种程度上。
披露:上述代码均未经过测试/执行,但希望足以传达概念;我也将不胜感激任何此类更正的评论:)
函数式编程是声明式编程的一种形式,它描述了计算的逻辑,完全不强调执行顺序。
问题:我想把这个生物从马变成长颈鹿。
每个项目都可以按任何顺序运行以产生相同的结果。
命令式编程是程序性的。状态和秩序很重要。
问题:我想停车。
必须完成每个步骤才能达到预期的结果。在车库门关闭时拉入车库会导致车库门损坏。
函数式编程是“用函数编程”,其中函数具有一些预期的数学属性,包括引用透明性。从这些性质,进一步的性质流动,特别是通过导致数学证明的可替代性启用的熟悉的推理步骤(即证明对结果的信心)。
因此,函数式程序只是一个表达式。
通过注意命令式程序中表达式不再具有引用透明性(因此不是用函数和值构建的,并且本身不能成为函数的一部分)的地方,您可以很容易地看到两种风格之间的对比。最明显的两个地方是: 突变(例如变量) 其他副作用 非本地控制流(例如异常)
在这个由函数和值组成的程序即表达式框架上,构建了语言、概念、“功能模式”、组合子以及各种类型系统和评估算法的完整实用范式。
根据最极端的定义,几乎任何语言——即使是 C 或 Java——都可以称为函数式,但通常人们将这个术语保留给具有特定相关抽象的语言(例如闭包、不可变值和语法辅助,如模式匹配)。就函数式编程的使用而言,它涉及使用 functins 并构建没有任何副作用的代码。用来写证明
• Imperative Languages:
Efficient execution
Complex semantics
Complex syntax
Concurrency is programmer designed
Complex testing, has no referential transparency, has side effects
• Functional Languages:
Simple semantics
Simple syntax
Less efficient execution
Programs can automatically be made concurrent
Simple testing, has referential transparency, has no side effects
我认为可以用命令式的方式来表达函数式编程:
if... else
/switch
语句 的状态检查这种方法存在巨大的问题:
函数式编程,将函数/方法视为对象并拥抱无状态,是为了解决我认为的这些问题而诞生的。
使用示例:Android、iOS 等前端应用程序或网络应用程序的逻辑,包括。与后端的通信。
使用命令式/过程代码模拟函数式编程时的其他挑战:
我还相信,最终,功能代码将被编译器转换为命令式/程序化的汇编或机器代码。但是,除非您编写汇编,因为人类使用高级/人类可读语言编写代码,否则函数式编程是列出的场景更合适的表达方式
//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
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
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:
Task: Calculate the 3rd fibonacci number. Steps:
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.
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:
Put a new sticky note on top of the old one:
Field | Value |
---|---|
Mail Address | The Fibonaccis |
Return Address | Otherwise, step 2, c/o The Fibonaccis |
Parameters | 2 (passing parameter-1) |
Return Value | undefined |
and send it.
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 |
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.
Task: Calculate the 3rd fibonacci number. Steps:
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.
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
我知道这个问题比较老,其他人已经很好地解释了,我想举一个简单的例子来解释相同的问题。
问题:写 1 的表。
解决方案: -
按命令式:=>
1*1=1
1*2=2
1*3=3
.
.
.
1*n=n
按功能风格:=>
1
2
3
.
.
.
n
命令式的解释我们更明确地编写指令,并且可以以更简化的方式调用。
在功能风格中,不言自明的东西将被忽略。