原文地址:Haskell学习-函数式编程初探

为什么要学习函数式编程?为什么要学习Haskell?

.net到前端,C#和JavaScript对我来说如果谈不上精通,最起码也算是到了非常熟悉的程度。这两门语言就像是我的盾牌和宝剑,给我保驾护航,开山劈石,伴随着我不断成长。同时C#和JavaScript它们本身也在不断地进化,不断出现越来越多方便的语法糖,但追根到底很多都是从函数式语言汲取的精华。比如高阶函数,lambada表达式,柯里化等。

于是从探险的角度,以好奇的心态开始学习函数式语言,探索这个宝库,拾取可供临摹的珍宝。最起码它能让你多一个不同的角度看待编程语言,影响你的思考方式。 学习的对象当然选择函数式语言的集大成者-Haskell。

什么是Haskell和函数式编程

Haskell 是一门纯粹函数式的语言。

函数式编程是面向数学的抽象,将计算描述为一种表达式求值。命令式编程是关于解决问题的步骤,函数式编程是关于数据的映射。在纯粹函数式程式语言中,你不是像命令式语言那样命令计算机「要做什么」,而是通过用函数来描述出问题「是什么」,也就是所谓范畴论中的映射关系。函数式语言有以下的特性:

  • 函数是一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合
  • 变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。
  • 函数式语言的条件语句,循环语句等也不是命令式编程语言中的控制语句,而是函数的语法糖
  • 惰性求值
  • 抽象数据类型
  • 灵活的多态
  • 高阶函数(Higher-order function)
  • 柯里化(Currying)
  • 闭包(Closure)

函数式编程的优点

函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。

  1. 函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。
  2. 由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发,能够更好地利用多个处理器(核)提供的并行处理能力。

Haskell基本语法

  1. 变量和函数

    一起介绍是因为在我看来,haskell中变量和函数是没有区别的。它们都是表达式,根据表达式的不同形式,分别对应到命令式语言中变量和函数的概念。 而且 haskell变量 赋值后就是不可变的,该 变量 就等于被赋予的值,与命令式语言中 变量 是内存地址的引用是完全不同的概念。 硬要对应的话它更像是 C# 中的不可变量 conststatic readonly

    你能从下面代码中区分出哪些是变量,哪些是函数吗?

      a = 1 -- 变量
    arr = map (*2) [1,2,3] -- 变量还是函数?
    maxNum = foldr max 0 -- 函数 --执行
    a
    > 1 arr
    > [2,4,6] maxNum [3,5,1]
    > 5

    定义函数: 函数名 参数 = 代码

    调用函数: 函数名 参数

    调用函数不用大括号( ),注意的是函数首字母不能大写。 还有maxNum看不到形式参数是因为柯里化可以去掉参数,后面会介绍。

  2. if else

    haskell中 if else 表达式中的 else 部分不能省略,也就是你不能只有 if 部分

    -- 等于小于大于0 分别对应 0,-1,1
    sign x = if x == 0 then 0
    else if x < 0 then -1
    else 1
  3. case of

    case of 表达式,与其他语言的switch case 类似。

    -- 求出列表第一项
    head' xs = case xs of
    [] -> "No head for empty lists!"
    (x:_) -> show x
    -- 执行
    head' "hello"
    >'h'
    head' [3,2,1]
    > 3
  4. 函数模式匹配

    函数模式匹配的方式定义 head',以及定义阶乘函数 factorial,它本质上就是 case of 的语法糖。函数模式匹配,减少了一大堆类似 if else 的判断逻辑,是我最喜欢的特性之一。

    -- 求出列表第一项
    head' [] = "No head for empty lists!"
    head' (x:_) = show x
    -- 阶乘
    factorial 0 = 1
    factorial n = n * factorial (n - 1) --执行
    head' [3,2,1]
    > 3
    factorial 5
    > 120
  5. guardswhere

    guards,类似 if else 表达式,但可读性更强,where语句定义的是局部变量表达式,它只能放在语句尾部,guards同样也是非常好的定义方式。

    bmiTell weight height
    | bmi <= 18.5 = "You're underweight,you emo,you!"
    | bmi <= 25.0 = "You're supposedly normal. Pffft,I bet you're ugly!"
    | bmi <= 30.0 = "You're fat! Lose some weight,fatty!"
    | otherwise = "You're a whale,congratulations!"
    where bmi = weight / height ^ 2
  6. let in

    let in 表达式,let 中绑定的名字仅对 in 部分可见。

    -- 圆柱体面积
    cylinder r h =
    let sideArea = 2 * pi * r * h
    topArea = pi * r ^2
    in sideArea + 2 * topArea

递归

  1. 我们使用递归来实现斐波那契数列和快速排序,haskell写的快速排序是我见过的最容易理解的版本了,专门为解决数学问题而生的 haskell 在解决算法和数据结构方面果然是不同凡响。

    -- 斐波那契数列
    fab 1 = 1
    fab 2 = 1
    fab n = fab (n-1) + fab (n-2) -- 快速排序
    quicksort [] = []
    quicksort (x:xs) =
    let smallerSorted = quicksort [a | a <- xs, a <= x]
    biggerSorted = quicksort [a | a <- xs, a > x]
    in smallerSorted ++ [x] ++ biggerSorted
  2. 尾递归实现常用的map和filter函数

  • [] 表示空列表

  • _ 匹配的是任意值。

  • (x:xs) 非常有用的列表匹配模式,x表示第一项,xs表示除去第一项之后的部分。使用(x:xs)可以方便的实现尾递归

    -- map
    map' f [] = []
    map' f (x:xs) = f x : map' f xs -- filter
    filter' _ []= [] -- _代表任意值
    filter' f (x:xs)
    | f x = x : filter' f xs
    | otherwise = filter' f xs

数据类型

了解了haskell基本语法后,我们再进一步了解haskell基本数据类型

  1. :type 获取任何表达式的类型,可以用简写形式 :t

  2. 基本数据类型

    • Int 表示整数
    • Integer 也是整数,但表示的是无界的,所以可以表示非常大的数
    • Float 表示单精度的浮点数
    • Double 表示双精度的浮点数
    • Bool 表示布尔值,它只有两种值:True 和 False
    • Char 表示一个字符。一个字符由单引号括起,一组字符的 List 即为字符串
    • List 列表中所有的项都必须是同一类型。
    • Tuple 的类型取决于它的长度及其项的类型。
    :t 1 -- Number
    1 :: Num p => p :t 1::Integer
    1::Integer :: Integer :t 1::Float
    1::Float :: Float :t False -- Bool
    False :: Bool :t 'c' --字符
    'c' :: Char :t "hello" -- 字符串
    "hello" :: [Char] :t [1,2,3] -- 列表list
    [1,2,3] :: Num a => [a] :t [("hi",1),("there",2)] -- Tuple
    [("hi",1),("there",2)] :: Num b => [([Char], b)]
    • 1::Integer 表示直接指定类型,如果不指定编译器会自动推导出类型,数字类型会推导出Number类型,它包括Int,Integer,Float,Double
    • [Char]String 表示的都是字符串类型
    • [1,2,3] :: Num a => [a] 列表中的 a 表示任意类型,意思你可以是Bool,Stirng,Int等等
    • [("hi",1),("there",2)] 这就是Tuple类型,列表里面的每个项都用 () 包起来,其中的每个项的元素数据类型必须相同,每个tuple中元素个数必须相等,但是每个tuple中的项可以不同类型,比如 ("hi",1) 中一个是字符串,一个是Int。
  3. 函数也有类型,定义函数的时候,加上参数的类型和输出类型是好习惯。

    • &&、||、not 表示与或非逻辑
    • == 表示等于
    • /= 表示不等于
    • ++ 连接列表,相当于concat
    • a, b这种类型参数,表示可以传入任何类型。
    • (Num a, Num p, Ord a) => a -> p=> 之前表示的是类型约束,这里的 a 限定只能是 Num 类型和 Ord 类型。Num表示数字类型,Ord则表示可比较大小的型别,包含如下三种型别之一:GT, LT, EQ。
    :t head -- 取列表第一项的函数
    head :: [a] -> a :t sign -- sign函数
    sign :: (Num a, Num p, Ord a) => a -> p :t (==) -- 是否相等
    (==) :: Eq a => a -> a -> Bool :t (++) -- 列表连接函数
    (++) :: [a] -> [a] -> [a] -- 执行
    sign 2
    > 1 head [3,2,1]
    > 3 "abc" == "bbc"
    > False "hello " ++ "world"
    > "hello world"

List 和 List comprehension

  1. 列表常用的函数

    null 列表是否为空

    length 返回列表长度

    head 返回列表第一个元素

    tail 返回列表除第一个元素以后的所有元素

    last 返回列表最后一个元素

    init 返回列表除最后一个元素之前的所有元素

    take n 返回列表前n个元素

    drop n 丢弃列表前n个元素

    maximum 返回最大的元素

    minimum 返回最小的元素

    sum 返回元素的和

    elem 元素是否包含于列表

  2. list range

    方便的range,尾递归加上list range,你真的还需要命令式语言中的循环语句吗?

    [1..10] -- 1到10的列表
    > [1,2,3,4,5,6,7,8,9,10] ['a'..'z'] -- a到z的字母字符串
    > "abcdefghijklmnopqrstuvwxyz" take 10 [1,3..] -- 前10个奇数
    > [1,3,5,7,9,11,13,15,17,19] take 10 (cycle[1,2,3]) -- 取前10的[1,2,3]序列
    > [1,2,3,1,2,3,1,2,3,1] take 5 $ repeat 3 -- 取前5项的3序列
    > [3,3,3,3,3] replicate 5 10 -- 相比 take repeat更方便的用法
    > [10,10,10,10,10]
  3. list comprehension

    list comprehension 相当于map 和 filter的函数的增强版, | 之前等于map, | 之后等于filter, 尤其在多限制条件和同时实现map,filter功能时更加明显。是个非常强大和有用的特性,完全可以替代列表的 map 和 filter 函数。

    list comprehension 其实是由 monadapplicative functor 生成的语法糖。

    [x*2 | x <- [1..10], x*2 >= 12] -- 取乘以 2 后大于等于 12 的元素, 等于map结合filter
    > [12,14,16,18,20] [if x `mod` 2 == 0 then "even" else "odd" | x <- [1..10]] -- 偶数转换为even,基数为odd, 等于map
    > ["odd","even","odd","even","odd","even","odd","even","odd","even"] [ x | x <- [10..20], x /= 13, x /= 15, x /= 19] -- 取除了13、15、19之外的元素,多个限制条件,等于filter
    > [10,11,12,14,16,17,18,20] [ x*y | x <- [2,5,10], y <- [8,10,11]] -- 求两个列表所有可能的组合
    > [16,20,22,40,50,55,80,100,110] -- 嵌套的列表, 在不拆开它的前提下除去其中的所有奇数
    let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]
    [ [ x | x <- xs, even x ] | xs <- xxs]
    > [[2,2,4],[2,4,6,8],[2,4,2,6,2,6]] --取得所有三边长度皆为整数且小于等于 10,周长为 24 的直角三角形
    [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]
    > [(6,8,10)]

参考资料

《HASKELL 趣学指南》

《Real World Haskell》

Haskell学习-函数式编程初探的更多相关文章

  1. go 学习笔记之学习函数式编程前不要忘了函数基础

    在编程世界中向来就没有一家独大的编程风格,至少目前还是百家争鸣的春秋战国,除了众所周知的面向对象编程还有日渐流行的函数式编程,当然这也是本系列文章的重点. 越来越多的主流语言在设计的时候几乎无一例外都 ...

  2. pyhton 学习 函数式编程

    函数是python内建支持的一种封装,我们通过把打断的代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计,函数就是面向过程的程序设计的基本单元 ...

  3. [学习] 从 函数式编程 到 lambda演算 到 函数的本质 到 组合子逻辑

    函数式编程 阮一峰 <函数式编程初探>,阮一峰是<黑客与画家>的译者. wiki <函数编程语言> 一本好书,<计算机程序的构造与解释>有讲到schem ...

  4. Python学习(26):Python函数式编程

    转自  http://www.cnblogs.com/BeginMan/p/3509985.html 前言 <core python programming 2>说: Python不大可能 ...

  5. Python学习总结之五 -- 入门函数式编程

    函数式编程 最近对Python的学习有些怠慢,最近的学习态度和学习效率确实很不好,目前这种病况正在好转. 今天,我把之前学过的Python中函数式编程简单总结一下,分享给大家,也欢迎并感谢大家提出意见 ...

  6. 翻译连载 |《你不知道的JS》姊妹篇 |《JavaScript 轻量级函数式编程》- 引言&前言

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 译者团队(排名不分先后):阿希.blueken.brucec ...

  7. 【大前端攻城狮之路】JavaScript函数式编程

    转眼之间已入五月,自己毕业也马上有三年了.大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户.同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺 ...

  8. 给 JavaScript 开发者讲讲函数式编程

    本文译自:Functional Programming for JavaScript People 和大多数人一样,我在几个月前听到了很多关于函数式编程的东西,不过并没有更深入的了解.于我而言,可能只 ...

  9. 如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ作者:杨昆 [编写高质量函数系列]中, <如何 ...

随机推荐

  1. Java虚拟机-内存tips

    java虚拟机内存可以分为独占区和共享区. 独占区:虚拟内存栈.本地方法栈.程序计数器. 共享区:方法区.Java堆(用来存放对象实例). 程序计数器 比较小的内存空间,当前线程所执行的字节码的行号指 ...

  2. 转载 Elasticsearch开发环境搭建(Eclipse\MyEclipse + Maven)

    概要: 1.使用Eclipse搭建Elasticsearch详情参考下面链接 2.Java Elasticsearch 配置 3.ElasticSearch Java Api(一) -添加数据创建索引 ...

  3. ES2015也就是ES6知识点持续更新

    ES6,全名:ECMAScript2015,先扯点其他的,ECMA是一个国际标准化组织,它最重要最重要的作用就是让ECMAScript这门语言标准化,什么意思呢?我们知道,js这门脚本语言是运行在浏览 ...

  4. 定制炫彩界面:duilib与MFC 的对比

    duilib是以DirectUI为技术原理开发的一款轻量级Windows桌面UI库,使用XML来描述界面风格,界面布局,可以很方便的构建高效,绚丽的,非常易于扩展的界面.从而很好的将界面和逻辑分离,同 ...

  5. Hibernate Session总结

    现在我们可以在 IDEA 下新建一个 Hibernate 项目,接着上次内容这次主要总结一下 Hibernate 的 Session,及其核心方法. Session 概述 Session 接口是 Hi ...

  6. 根据appId匹配项目名称

    有时候后端返回的接口中也许没有我们想要的字段,可以通过下面的方式拿到想要的字段 代码如下: //获取项目名称 getBizName(appId) { let proNameList = this.$s ...

  7. PAT1004:Counting Leaves

    1004. Counting Leaves (30) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue A fam ...

  8. java里常用的redis客户端简介

    Redis的各种语言客户端列表,请参见Redis Client.其中Java客户端在github上start最高的是Jedis和Redisson.Jedis提供了完整Redis命令,而Redisson ...

  9. sqlserver两种分页方法比较

    -- 3000 page(从1开始) 10 pagesize -- 方法1(效率不高): SELECT TOP 10 * FROM [xxx].[oooo] WHERE id NOT IN (SELE ...

  10. sap 内表

    内表的目的在ABAP/4中,主要使用表格.表格是R/3系统中的关键数据结构.长期使用的数据存储在关系数据库表格中.关于如何读取和处理数据库表格的详细信息,参见读取并处理数据库表.除了数据库表格,还可以 ...