用Scala学习函数式编程


在这篇博文中,我将写到我用Scala学习函数式编程的历程。我有一个非常面向对象的Java背景。为了陪伴我走过这段旅程,我决定参加 这个Coursera课程

我觉得学习理论是一回事,应用理论又是一回事。出于这个原因,我也决定在完成课程后建立一个工作应用。这将在最后几章中进一步详述。

所以我的目标是。

  • 完成Coursera课程的作业,其中有大学水平的内容
  • 编写一个反映我在这个课程中学到的一些东西的应用程序

为什么是Scala?

有几个原因。

  1. 与Java的互操作性。这让我可以使用我最喜欢的库。
  2. 我真的很想掌握更多的函数式编程原理
  3. 荷兰的就业市场似乎需要Scala开发者
  4. 根据StackOverflow 2020,它是收入最高的语言之一 :D
  5. 需求量+高薪=利润?

第一周

整体语法

Scala语言让我想起了很多Kotlin。类型的书写格式是 "var: 类型 "的格式。Scala中的很多东西都是隐含的。例如,一个函数的最后一个表达式就是它的返回。

一切都是表达式

在Scala中,一切都是表达式。 即使是if-语句!   这对一个Java开发者来说可能是令人震惊的。   幸运的是,我有一些编写Kotlin的经验,所以这个概念对我来说并不陌生。下面是一个if表达式的例子。

   def abs(x: Double) = if (x > 0) x else -x    

函数、块和词汇范围

Scala的函数可以存在于函数中。这可以用来隐藏一个函数的部分表面区域,同时还可以重新使用一个私有函数。下面是一个通过近似和递归来计算一个数字的平方根的例子。abs'、'isGood'、'improve'和'sqrtIter'函数在函数的范围之外是不能访问的。

   def sqrt(x: Double): Double = {
    def abs(x: Double) = if (x > 0) x else -x

    def isGood(guess: Double, x: Double): Boolean =
      abs(guess * guess - x) < 0.0001

    def improve(guess: Double, x: Double) =
      (guess + x / guess) / 2

    def sqrtIter(x: Double, guess: Double): Double =
      if (isGood(guess, x)) guess else sqrtIter(improve(guess, x), x)

  sqrtIter(x, 1.0)
}    

Def vs Val

Scala知道定义(defs)和值(vals)之间有很大区别。defs只是定义了一个变量应该被评估的对象。这意味着defs遵循一种懒惰的评估策略,在需要时对def进行评估。另一方面,Vals是当场评估的。它正确地反映了一个变量的值是什么。

不变性

函数式程序包含了不变性的概念。这意味着你不会改变数据的状态(如 person.setName(String) ),而是返回数据的新版本(如 person.copyWithName(String) )。这看起来效率很低,但它赋予了一堆的好处。

  1. 线程安全 : 根据定义,不可变的对象是线程安全的
  2. 没有无效的状态 : 一旦你验证了一个对象的初始状态,你就可以保证这个对象始终保持有效。
  3. 测试更简单 : 因为对象内部不发生变化,所以到目前为止,它们更容易测试,因为它们的方法总是可重复的。

纯粹的函数

函数式编程包含了函数应该是纯粹的想法。这意味着

  • 该函数不得依赖其范围之外的任何代码,或隐藏在视野之外。
  • 该函数不得修改其范围之外的任何代码。
  • 在参数相同的情况下,该函数必须总是评估为相同的结果。

递归

递归对我来说不是一个新概念。但有时,我发现我很难把它弄清楚。鉴于在纯函数式编程中没有循环的概念,这将会很有趣。在下面的练习中,你会看到我使用递归做了以下事情。

  1. 实现Pascal's Triangle的逻辑
  2. 在一个字符列表中平衡小括号
  3. 计算变化的可能性,给定一定的硬币类型列表
  4. 计算阶乘
   // Excercise 1
def pascal(c: Int, r: Int): Int = 
  if (c == 0 || r == c) 1 else pascal(c - 1, r - 1) + pascal(c, r - 1)

// Excercise 2
def balance(chars: List[Char]): Boolean = {

  def loop(i: Int, amountOpen: Int): Boolean =
    if (i >= chars.size) amountOpen == 0
    else if (amountOpen < 0) false
    else if (chars(i).equals('(')) loop(i + 1, amountOpen + 1)
    else if (chars(i).equals(')')) loop(i + 1, amountOpen - 1)
    else loop(i + 1, amountOpen)
  
  loop(0, 0)
}

//Exercise 3
def countChange(money: Int, coins: List[Int]): Int ={
  if (money < 0 || coins.isEmpty)
    0
  else if (money == 0)
    1
  else
    countChange(money - coins.head, coins) + countChange(money, coins.tail)
}

//Excercise 4
def factorial(n: Int): Int = 
  if (n == 0) 1 else n * factorial(n-1)    

尾部递归

递归会在你的堆栈跟踪中产生大量的负载,导致堆栈溢出错误。编译器和运行时已经实现了尾部递归的概念来解决其中的一些问题。尾部递归会回收原始的调用图,有效地使调用成为变相的for循环。只有当递归调用是一个函数中的最后一个表达式,并且没有其他操作符应用于递归调用时,才可以应用尾部递归。请注意,尾部递归引入了另一个层次的认知负荷,因为函数通常变得更难读。

第二周

高阶函数

高阶函数是以其他函数为参数和/或返回其他函数的函数。这里有一个简单的例子。比方说,我有这些函数。

   // Returns the input
def identity(x: Int) = x

// Calculates  cube  of  a  number
def cube(x: Int) = x * x * x

// Calculates factorial of a number
def factorial(x: Int): Int =
  if (x <= 1) 1 else x * factorial(x - 1)


//Sum function, adds up all ints between a and b
def sum(a: Int, b: Int): Int =
  if (a > b) 0 else a + sum(a + 1, b)    


如果我想写一个函数,将一个数字的所有立方或阶乘相加,我可以这样写。

   //Wraps all operands with a high-order function (the f parameter)
def sum(f: Int => Int, a: Int, b: Int): Int =
  if (a > b) 0 else f(a) + sum(f, a + 1, b)

// Create a cubical sum of all ints from a to b
def sumCube(a: Int, b: Int) = sum(cube, a, b)
// Same for factorial
def sumFactorial(a: Int, b: Int) = sum(factorial, a, b)    

咖喱

高阶函数实现了咖喱的概念。*在此插入关于印度食物的蹩脚笑话*。这基本上是嵌套返回函数的概念。 一个函数可以返回一个函数,而这个函数可以返回另一个函数

   // Creates a curried version of sum, where the
// first function is that to be applied to the input and  the
// second function is the input
// (Int => Int) => (Int, Int) => Int
def sumCurry(f: Int => Int): (Int, Int) => Int = {
  def sumF(a: Int, b: Int): Int =
    if (a > b) 0 else f(a) + sumF(a + 1, b)

  sumF
}

// Or with syntax sugar.....
def sumCurry(f: Int => Int)(a: Int, b: Int): Int =
  if (a > b) 0 else f(a) + sumCurry(f)(a + 1, b)

// Sum can now be called like this. Pretty clean right?!
sumCurry(cube)(1, 6)
sumCurry(factorial)(1, 6)    


现在让我们看看是否可以用咖喱技术重写阶乘。

   // Here is how I would curry a product function. I really like the word curry, btw.
def product(f: Int => Int)(a: Int, b: Int): Int =
  if (a > b) 1 else f(a) * product(f)(a + 1, b)

// Factorial can now be written as follows
// Because factorial is by definition the product of all numbers 1 to n
def factorial(n: Int) = product(identity)(1, n)

// True!
factorial 

== 120

函数集

在第二周结束时,我 实现了函数集的概念 。功能集的一般概念是:。 我们用一个集合的特征函数来表示它,即它的   ` 包含 ` 谓词。   这是一个相当有趣的作业,因为它真正显示了函数式编程的优雅

第三周

Traits, Generics, Classes and Objects. 咦?

在课程中,我学会了如何在Scala中使用基本的面向对象基元。我对这些概念非常熟悉。但是,在课程中我注意到,老师也在他的代码中应用了更多的声明性/功能性原则。例如,他用类和对象实现了一个二叉树。然而,他并没有使用命令式代码来实现。他没有更新树的状态,而是返回一个新的树的实例。这在本质上是 纯粹的 功能。哇。没有副作用。数据是不可改变的。但是,哇。这是我的一个不可变的列表的例子。

   trait List[T] {
 def isEmpty:Boolean
 def head: T
 def tail: List[T]
}

class Cons[T] (val head: T, val tail: List[T]) extends List[T] {
  def isEmpty = false;
}

class Nil[T] extends List[T] {
  def isEmpty = true
  def head = throw new NoSuchElementException("Empty")
  def tail = throw new NoSuchElementException("Empty")
}    

第四周

分解与模式匹配

我经常使用分解。我在开发 自己的开源项目 时遇到的一个问题是表达式的简化问题。事实证明,对于我想做的事情,使用分解并不是一个可行的解决方案。

模式匹配帮助你以一种非常方便和紧凑的语法来分解和浏览数据结构。它看起来像一个Java开关语句,但绝对不是。 开关永远不可能!   模式匹配功能在我看来非常漂亮。

模式匹配列表

你也可以对列表、字符串甚至是RegEx模式进行模式匹配。这里我使用模式匹配来实现插入排序。正如你所看到的,我使用'::'对列表的头部和尾部进行匹配。

   def insertionSort(xs : List[Int]) : List[Int] = {
  xs match{
    case Nil => Nil
    case x :: xs1 => insert(x, isort(xs1))
   }
 }

def insert(x : Int, xs : List[Int]) : List[Int] = {
  xs match {
      case Nil => List(x)
      case y :: xs1 =>
          if(y >= x) x :: xs
          else y :: insert(x,  xs1)
   }
}    

第五周

哈夫曼编码

在第五周,我实现了(而且几乎不理解)哈夫曼编码。它本质上是一种压缩和解压字符串的方法,为字符串中的每个字符分配一个权重。这个权重代表一个字符在一个字符串中的使用次数。这些字符被放入一个特定的代码树中,其中包含叉子节点和叶子节点。

这是我的解决方案(得到8/10分)

图ples

图ples是一个在Java世界中不存在的概念。它们允许你定义一个数据结构,允许你从一个函数中返回多个值。这是我最喜欢的Scala语言的一个部分。然后你可以对图元进行去结构化或模式匹配,就像我在这个合并排序实现中所做的那样。

   def mergeSort(xs: List[Int]): List[Int] = {
  val n = xs.length / 2
  if (n == 0) xs
  else {
    val (firstHalf, secondHalf) = xs splitAt n
    merge(mergeSort(firstHalf), mergeSort(secondHalf))
  }
}

def merge(xs: List[Int], ys: List[Int]): List[Int] =
  (xs, ys) match {
    case (Nil, ys) => ys
    case (xs, Nil) => xs
    case (x :: xTail, y :: yTail) =>
      if (x < y) x :: merge(xTail, ys) else y :: merge(yTail, xs)
 }    

Map, Filter, Reduce

我向你介绍,Map-Filter-Reduce的三明治。

信用:   https://thenewstack.io/dont-object-ify-me-an-introduction-to-functional-programming-in-javascript/

这三个函数(连同forEach)构成了大多数函数操作的基础。

Map 允许你转换数据。 Filter 允许你剔除不需要的数据。 Reduce 允许你对数据进行总结。

这三种方法都非常强大。而且它们构成了你可以对数据序列进行处理的基础。

第六周。云上的Scala

在我学习Scala的最后一周,我决定建立一个在云上运行的应用程序。考虑到这是函数式编程,我决定以云函数的形式来做。我想创建一个服务,分析Word、PDF和文本文件的内容并提取这些文件的关键信息。该程序基本上会对文件的文本内容进行情感分析,并回应以下内容。

  1. 文件的语言
  2. 幅度最大的句子
  3. 最积极的句子
  4. 最消极的句子
  5. 文件的总体基调

这项服务将使用谷歌云自然语言API来进行分析。

使用了函数式编程原则

我在我的程序中应用了以下原则, 位于这里 :

  • 创建和分解图元
  • 高阶函数
  • 咖喱
  • RegEx模式匹配
  • 嵌套函数
  • 超棒的Scala语法糖。比如那个看起来很酷的 "else-try "块

我觉得这些原则反映了我对函数式编程最喜欢的部分。

结论

我觉得我已经学到了很多关于函数式编程的知识。起初,我并不喜欢它,因为最初的学习曲线。我觉得一切都隐藏在一些奇怪的符号后面。在我理解了其中一些符号的含义后,我对函数式编程有了很大的体会。在我的职业生涯中,我肯定会更多地使用这些实践。谁知道呢,我甚至可能在一个专业项目中使用Scala。

与我联系 ❤  

我通常都会为一个拼写会话而感到高兴。甚至是新的代码的朋友?