面向函数的编程总结

从闭包谈起

闭包的原文是Closure。也就是“闭”。包这个字是翻译者私自加上的。闭包最显而易见的,就是其函数嵌套函数的写法。一个典型的闭包代码如下

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()

因为是“闭”,上述代码隐藏了一个变量(local)。显然,如果你希望别人随便访问这个变量,就只能把这个变量写成一个局部变量。然而,你又希望别人按照你定义的方式可以访问这个变量。上述代码之所以能运行,是因为JS的作用域————函数内部可以访问外部的变量。java里也有类似的设定。那就是内部类能够访问外部类的所有属性和方法。

public class Milk {
	public final static String name = "纯牛奶";//名称
	private static int num = 16;//数量
	public Milk()
	{
		System.out.println(name+":16/每箱");
	}
	/**
	 * 闭包
	 * @return 返回一个喝牛奶的动作
	 */
	public Active HaveMeals()
	{
		return new Active()
				{
					public void drink()
					{
						if(num == 0)
						{
							System.out.println("木有了,都被你丫喝完了.");
							return;
						}
						num--;
						System.out.println("喝掉一瓶牛奶");
					}
				};
	}
	/**
	 * 获取剩余数量
	 */
	public void currentNum()
	{
		System.out.println(name+"剩余:"+num);
	}
}

/**
 * 通用接口
 */
interface Active
{
	void drink();
}
/**
 * 测试Main方法
 */
public class Person {
	public static void main(String[] args) {
		//买一箱牛奶
		Milk m = new Milk();
		Active haveMeals = m.HaveMeals();
		//没事喝一瓶
		haveMeals.drink();
		//有事喝一瓶
		haveMeals.drink();
		//看看还剩多少?
		m.currentNum();
	}
}

原来虽然我们从不把内部类的用法叫做闭包。但是我们的代码里充斥着关于闭包的用法。比如安卓的点击事件处理

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Toast.makeText(MainActivity.this, "Hello World!", Toast.LENGTH_SHORT).show();
    }
});

处于对JAVA的简洁性和一贯性的原因,JDK设计人员一直拒绝把函数也当作对象。所以,才必须实现一个接口,把一个函数伪装成一个对象。我认为,函数不是一个对象,这应该是JAVA所剩不多的底线之一。

Lambda 表达式

在JAVA里,如果我们提到闭包,第一个想到的就是Lambda 表达式。语法非常简洁。如果只有一个参数,则可以省略括号。如果可以推断出类型,则可以在参数列表中省去类型。

(参数) -> 表达式

函数式【接口】(FunctionalInterface)

我们举ArrayList的sort方法。

    @Override
    @SuppressWarnings("unchecked")
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

我们注意到,没有变化。仍然是接受一个Comparator对象。这不过Comparator接口发生了一个变化。增加了一个注解@FunctionalInterface。注意,这个注解只是强制编译器检查下面约束

1. 函数式接口的方法中有且只能有个一个抽象方法,只有方法定义,没有方法体
2. 在接口中覆写Object类中的public方法,不算是函数式接口的方法。

比如说如下这个Comparator接口。

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);//唯一一个
    boolean equals(Object obj);//覆盖compare类中的方法
}

这个注解表示,可以使用一个Lambda表达式作为接口的一个实现。

方法引用

java为了进一步简化,引入方法引用的概念

button.setOnClickListener(event -> System.out.println(event));

可以进一步被简写为

button.setOnAction(System.out::println);

面向流的编程

JDK8中引入了流的概念。下面两个代码等价。

long count = 0;
for (Person p : persons) {
    if (p.getAge() > 20) {
        count ++;
    }
}
long count = persons.stream()
                    .filter(person -> person.getAge() > 20)
                    .count();

据说使用Stream可以充分利用多核CPU。

函数式编程

函数式编程关心数据的映射,命令式编程关心解决问题的步骤。
简介是函数式编程最大的特点。不过我们当然不是为了省键盘选择去磨脑子。两大好处,第一个是扔给程序员的甜头————更容易并行了。第二个则是送给程序员的毒酒————代码写成这样可以方便的进行数学研究。数学家对这群干活慢BUG多擅长装逼的程序员容忍到了极限了。不过程序员并非是傻子。haskell是人类公认的语法最为简介而优美的语言。前提是你理解什么叫单子就是自函子范畴中的独异点。
在函数式编程中,一个循环可以写成(实际上是一个递归实现)

factorial n = if n > 1

              then n * factorial (n-1)

              else 1

正常写其实是这样的

long factorial (int n)
{
  long result = 1;
  while (n > 1)
    result *= n--;
  return result;
}

函数式编程的特点如下

  • 函数结果只依赖输入不依赖于上下文, 使得每个函数都是一个高度独立的单元, 便于进行单元测试和除错.

  • 函数结果不依赖于上下文也不修改上下文, 从而在并发编程中不需要考虑线程安全问题, 也就避免了线程安全问题带来的风险和开销. 这一特性使得函数式程序很容易部署于并行计算和分布式计算平台上。

柯里化

函数的柯里化(currying)是指将一个接受n个参数的函数变成n个接受一个参数的函数.

def add(x:Int, y:Int):Int = {return x+y}

等价于

scala> def add(x:Int)= (y:Int)=>x*y
add: (x: Int)Int => Int

个人看法

面向函数的编程可以完全替代命令式编程。
但是,这并不意味着这种编程很容易。面向函数编程之所以能解决并发的问题,是因为这种语法本身就强迫你在写代码时想清楚并发问题再动笔。
并发的实质是多个CPU的并行计算和单个CPU的分时复用。CPU是命令式的,所以人类顺理成章的按照命令式的思维构建了自己的编程理念。时至今日再来探讨是否当初错了,似乎已经为时太晚。
面向函数的编程一直广为诟病的是其效率问题。但是实际上这种思路的效率并没有硬伤。只是其代码严重依赖优化,而整个编程社会完全没有准备好面向函数编程的基本组件,也没有足够的投入去优化代码。
还是那句话,编程世界唯一重要的是算法,而非形式。算法能做到的事情就一定能实现。至于如何实现是最合适的,最优雅的,不过是成王败寇罢了。

你可能感兴趣的:(行业概况)