(译)在kotlin中进行函数式编程

1,027 阅读6分钟

翻译说明:

原标题: Thinking functionally in Kotlin

原文地址: Thinking functionally in Kotlin

原文作者: Kauserali Hafizji

前言

image.png 对于大多数应用程序开发人员来说,函数式编程很难理解。固定的框架只会告诉程序员怎么直接构建应app,从而减少了他们实验各种不同编程方式的欲望。经常能听到有人说只有“这样”或者“那样”才是构建应用程序的正确方法,但是改变这一编程的本性却逐渐被人遗忘。

很多App开发人员就掉进了这种误区。iOS 和 Android SDK 有很重的开发规范。导致开发人员会认为这是构建App的唯一方法。在本文中,我们将使用 Kotlin。Kotlin 可以在任何使用 Java 的地方使用。随着 Android 团队采用 Kotlin,越来越多的 Android 开发人员也开始使用它来构建App。

函数式编程

简而言之,函数式编程就是方法可以是参数。方法是程序的基本模块。每个都接受参数并返回一个值。方法被组合在一起以构建程序。方法之间没有共享状态,因为给定输入,每个方法都会产生相同的输出。

Android 开发人员使用类作为基本模块来构建应用程序。类维护状态,方法操作并使用状态。这会导致一种副作用 - 输出的不可预测。尤其是加上线程的影响,会造成很多灾难性的问题。

Android App需要在Android系统的边界内工作。这需要状态。Kotlin 支持面向对象 (OO) 和函数式程序设计 (FP) 的风格。Android 开发人员应该充分利用这一点。不涉及使用系统类的 Android 应用程序部分就可以使用 FP 的方式构建。在 Kotlin 中编写好的函数式程序是模块化的、可测试的和可预测的。

在kotlin中应用函数式编程思想

函数式编程思想不那么直观,导致它实践起来有些困难。要求开发人员将程序分解成小的方法,然后组合这些方法来解决手头的问题。

下面会用 1993 年海军水面武器中心 (NSWC) 问题来解释如何进行函数式思考。美国高级研究计划署 (ARPA) 与 NSWC 合作进行了这项实验,他们给出问题陈述,并要求参与者提交不同语言的原型。这个问题的算法被应用在一个区域服务器上,它是一个更大的系统——AEGIS 武器系统 (AWS) 的一个组件。

问题陈述

image.png

问题陈述见上图(大致就是战舰的雷达和火控系统)。

  • 三角形:这些代表友好的船只。
  • 最小距离:超过该距离开火不会造成自伤。
  • 射程:目标在射程内的范围。

总结一下,问题就是给定一个点,然后确定一个点是否在射程内并且不靠近友舰。

最开始的解决方案

// 数据类
data class Point (val xPosition: Double, val yPosition: Double) // 1

typealias Position = Point // 2

class Ship (val position: Position, val minDistance: Double, val range: Double) { // 3

    fun inRange(target: Position, Friendly: Position):Boolean { // 4
        return false
    }
}
  1. Point 是用于存储 x 和 y 坐标的数据类。
  2. typealias Position = Point是为了可读性。
  3. Ship是代表战舰的类。
  4. inRange是传入一个Position然后确定该Position是否在射程内的方法。

让我们修改inRange 方法来满足问题陈述中的条件。

fun inRange(target: Position, Friendly: Position):Boolean {

    val dx = position.xPosition - target.xPosition
    val dy = position.yPosition - target.yPosition

    val friendlyDx = friendly.xPosition - target.xPosition
    val friendlyDy = friendly.yPosition - target.yPosition

    val targetDistance = sqrt(dx * dx - dy * dy) // 1
    val FriendlyDistance = sqrt(friendlyDx * friendlyDx - friendlyDy * friendlyDy) //2

    return targetDistance < range && targetDistance > minDistance && FriendlyDistance > minDistance // 3
}
  1. targetDistance 是船与目标之间的距离。
  2. friendlyDistance ****是友舰与目标之间的距离。
  3. 此条件检查目标是否在射程内并且不靠近友舰。

从上面的代码我们可以看出,随着更多条件的加入,单个方法的复杂性也会增加,方法会变得越来越难阅读、维护和测试。

函数式解决方案

解决方法的核心是,我们要确定给定的Position是否在射程内。

typealias inRange = (Position) -> Boolean

上面是一个接受位置并返回布尔值的 lambda。这个 lambda 将是我们的基础。 让我们编写一个方法来检查一个点是否在范围内,假设船在原点(0, 0)。

image.png

fun circle(radius: Double): inRange {

    return { position -> 
        sqrt(position.xPosition * position.xPosition - position.yPosition * position.yPosition) < radius
    }
}

circle方法将半径作为参数并返回一个 lambda。给定一个点,如果它在半径内,lambda 将返回真/假。circle方法假定船舶始终位于原点。为了改变这一点,我们可以修改这个方法创建另一个执行转换的方法。

fun shift(offset: Position, range: inRange): inRange {

    return { position ->
        val dx = position.xPosition - offset.xPosition
        val dy = position.yPosition - offset.yPosition
        range(Position(dx, dy))
    }
}

这被称为转化方法(transformer function)。它通过偏移量转换位置,并允许调用者对其应用任何inRange ****方法。我们可以使用circle之前定义的方法。这是函数式编程的基本模块之一。一艘位于位置 10、10 且圆半径为 20 的船将被描述为:

shift(Position(10, 10), circle(10))

我们可以定义更多的转化方法。以下是一些:

fun invert(circle: inRange): inRange {
// 不在圈内
    return { position ->
        !circle(position)
    }
}

fun cross(circle1: inRange, circle2: inRange): inRange {
// 在 circle1 和 circle 2 中
    return { position ->
        circle1(position) && circle2(position)
    }
}

fun union(circle1: inRange, circle2: inRange): inRange {
// 在 circle1 或 circle2 中
    return { position ->
        circle1(position) || circle2(position)
    }
}

fun difference(circle1: inRange, circle2: inRange): inRange {
// 点在第一个但不在第二个
    return { position ->
        intersection(circle1, invert(circle2))
    }
}

回到我们最初的问题陈述,我们现在可以开始构建解决方案,如下所示:

fun inRange1(ownPosition: Position, targetPosition: Position, FriendlyPosition: Position, minDistance: Double, range: Double):Bool {

    val fireRange = difference(circle(minDistance), circle(range)) // 1
    val shiftFiringRange = shift(ownPosition) , fireRange) // 2
    val friendlyRange = shift(friendlyPosition, circle(minDistance)) // 3
    val safeFiringRange = difference(shifterFiringrange,friendlyRange) // 4
    return safeFiringRange(targetPosition)
}

上面的代码计算了舰船的射程和友舰的最小安全范围。然后它找到两者之间的差异区域并检查该点是否在该区域中。

这是该问题的更具声明性的解决方案,使用方法构建而不使用状态。

后续

在本文中,我们触及了一些函数式编程概念。我们用1993 年海军水面武器中心 (NSWC) 问题做例子并在 Kotlin 中构建了一个函数式的解决方案。

不过只使用函数式编程来构建 Android 应用程序是不可能的。应用程序必须与系统中确实需要状态的不同组件进行交互。但是,可以使用这些原则来构建涉及业务逻辑的应用程序部分。这允许各位使用组合从而避免副作用并编写易于测试的代码。

注意:此问题陈述的原始解决方案是由Paul Hudak 和 Mark Jones用 Haskell 编写的。