最近接近接触到函数式编程的范式,深感它实在是非常有用。其最突出的特点是No side effect, no variables, no loops。在使用函数式编程的程序中,可以不包含一个任何一个循环和变量,同时无附带效应也使得出Bug的几率低了不少,调试起来也更加简单。这就使得编程的人更能集中精力思考如何解决问题而不是操心怎么用计算机实现。而Mathematica是老牌的科学计算软件,虽然冠以科学计算之名,但实际上它能处理的问题非常多,并不仅限于科学。同时它内建了丰富的函数库,在解决问题时也更灵活。个人认为完全可以取代python之类的编程语言。虽然python也有众多的函数库,但最大的问题是它们在风格上并不统一,参考文档也参差不齐,在学习上就有比较大的困难。
而Mathematica原生支持函数式编程,与丰富的库函数强强结合,简直是学习生活的不二之选。在Mathematica中的函数式编程风格有几个特点:
万物皆表达式
Mathematica中的所有语句都是表达式,那我们就可以把多个表达式整合到一个表达式中进行计算。所以用C++写出来的多个语句,在Mathematica中可以只用一个表达式完成。不过这样可能会带来可读性上的问题,因此可以适当地对表达式进行拆分,写成多个。
任何数据结构都是表
表在Mathematica中居于十分核心的地位,因为任何一种数据结构都可以等价为一个多维表,那么对表进行计算和操作也就是十分自然的了。Mathematica对表运算进行了特别的优化,因此建议多使用表。在对表的运算中,Map是一类核心的函数,事实上在任何一种函数式编程或类似的语言都能找到它的身影,诸如R语言的Apply,Matlab的Arrayfun这样的函数其在思想上和Map是一样的,就是将函数f应用于表中的每一个元素。
举个例子,我们想寻找一100以内的所有质数,用函数式编程的思想来做,我们就可以先生成一个1-100的整数表A,然后写一个函数f判断某一个数是否为质数,接着将这个函数Map到表上,从而得到一个长度为100的True和False组成的表B,接着从表A中提取表B对应位置为True的元素。
Map函数的正式语法为:
Map[f,expr] or f/@expr
其中的expr就是所要映射的表。同时如果有的表有多个层次,那么可以指定映射函数到哪一个层次的元素,即Map[f,expr,levelspec]。
对于刚开始的问题,我们用原生的PrimeQ作为判断函数f,那么就可以用PrimeQ/@Range[1,100],得到了表B,其中Range[1,100]表示生成从1到100的表。接着要判断哪些位置上的元素为True,此时需要用到Position函数,Position[PrimeQ/@Range[1,100],True]就表示表B中为True的元素的位置,然后用Extract[expr,list]提取expr中的由list指定的位置上的元素。
将以上程序写在一起就是
Extract[Range[1,100],Position[PrimeQ/@Range[1,100],True]]
就会得到{2,3,5...}这样一个100以内所有质数的表。
注意到上式中出现了两次Range,这就意味着程序会进行两次计算。因此我们可以先用一个变量存储s,写成如下的形式:
Extract[s=Range[1,100],Position[PrimeQ/@s,True]]
避免了二次计算。值得注意的是,与Haskell这样的函数式编程语言所使用的Lazy evaluation不同的是,在计算以上的语句时,Mathematica采取的策略是先计算能算的,因此它先计算的是s=Range[1,100],再计算内部的Position,所以给s赋值要写在Position外部。如果我们写成
Extract[s,Position[PrimeQ/@(s=Range[1,100]),True]]
这样是会出错的,因为s先于Position函数计算,此时它不知道s是什么。
与Map同系列的函数还有Mapthread、MapIndexed、MapAt、MapAll。就经验来看,Mapthread和MapIndexed使用得较其它两个更多。
MapIndexed顾名思义就是带目录的映射,Map[f,{a,b}]的作用是生成{f[a],f[b]},而MapIndexed[f,{a,b}]就是生成{f[a,{1}],f[b,{2}]}。不过值得注意的是在传递参数时,目录是作为一个单元素表{1}、{2}这样的形式进行传递的,因此在原函数中就要对第二个参数用First[]函数提取。
MapThread是用于带多个参数的函数进行映射。比如MapThread[f,{a1,a2,a3...},{b1,b2,b3...}]得到的就是{f[a1,b1],f[a2,b2],f[a3,b3]...}。事实上这两个函数可以都用Map实现。MapThread[f,{a1,a2,a3...},{b1,b2,b3...}]与Map[f,{{a1,b1},{a2,b2},{a3,b3}....}]的效果是差不多的。而MapAt可以指定将函数映射在某个或多个元素的位置上。MapAll可以将函数映射到表中的每个元素的每个子表达式上。MatAll[f,{a,{b}}]就可以实现{f[a],f[f[b]]}。,其中的元素{b}有两个层次 ,一是外部的表,二是内部的元素,MapAll就映射两次。