在Scala中将多个函数定义为相同类型的惯用方式是什么?

时间:2019-01-18 00:18:51

标签: scala functional-programming idioms idiomatic

我是一位在ruby,python和javascript(特别是后端node.js)方面经验丰富的程序员,我曾在java,perl和c ++中工作,并且在学术上使用过lisp和haskell,但是我是全新的到Scala并尝试学习一些约定。

我有一个接受函数作为参数的函数,类似于排序函数接受比较器函数的方式。实现此目的的更惯用的方法是什么?

假设这是接受函数参数y的函数:

object SomeMath {
  def apply(x: Int, y: IntMath): Int = y(x)
}

应该将IntMath定义为trait,并且将IntMath的不同实现定义在不同的对象中吗? (我们将此选项称为A)

trait IntMath {
   def apply(x: Int): Int
}

object AddOne extends IntMath {
   def apply(x: Int): Int = x + 1
}

object AddTwo extends IntMath {
  def apply(x: Int): Int = x + 2
}

AddOne(1)
// => 2
AddTwo(1)
// => 3
SomeMath(1, AddOne)
// => 2
SomeMath(1, AddTwo)
// => 3

还是IntMath应该是函数签名的类型别名? (选项B)

type IntMath = Int => Int

object Add {
  def one: IntMath = _ + 1
  def two: IntMath = _ + 2
}

Add.one(1)
// => 2
Add.two(1)
// => 3
SomeMath(1, Add.one)
// => 2
SomeMath(1, Add.two)
// => 3

但是哪一个是惯用的scala?

还是都不是惯用的? (选项C)

我以前在函数式语言中的经验使我趋向于B,但是我从未在scala中见过。另一方面,尽管trait似乎增加了不必要的混乱,但我已经看到了该实现,并且它在Scala中的工作似乎更加顺畅(因为该对象可以使用apply函数进行调用)。

[更新]修复了示例代码,其中将IntMath类型传递给SomeMath。 scala提供的句法糖(使用函数object调用apply就像函数一样)给人一种幻觉,即AddOneAddTwo是函数,并且像选项A中的函数一样传递

2 个答案:

答案 0 :(得分:3)

由于Scala具有显式的函数类型,因此我想说,如果您需要将函数传递给函数,请使用函数类型,即您的选项B。为此,它们明确存在。

BTW表示您“从未在scala中从未见过”是什么意思,这还不是很清楚。许多标准库方法接受函数作为其参数,例如集合转换方法。创建类型别名以传达更复杂类型的语义也是完全惯用的Scala,并且别名类型是否为函数并不重要。例如,我当前项目中的核心类型之一实际上是函数的类型别名,该函数使用描述性名称来传达语义,并且还允许轻松地传递符合条件的函数而无需显式创建某些特征的子类。 / p>

我认为,了解apply方法的语法糖与如何使用函数或功能特性没有任何关系也很重要。实际上,apply方法确实具有仅用括号来调用的能力。但是,这并不意味着具有apply方法的不同类型即使具有相同的签名也可以互操作,并且在这种情况下,我认为这种互操作性很重要。轻松构造此类实例的能力。毕竟,在您的特定示例中,无论您是否可以在IntMath上使用语法糖,对于您的代码来说都很重要,但是对于您代码的用户而言,可以轻松地构造以下内容的实例IntMath以及传递IntMath已经存在的现有事物的能力,非常重要。

使用FunctionN类型,您将能够使用匿名函数语法构造这些类型的实例(实际上是几种语法,至少包括以下几种:x => y,{{1 }},{ x => y }_.xmethod _)。在Scala 2.11之前甚至没有办法创建“单一抽象方法”类型的实例,甚至在那里也需要一个编译器标志才能真正启用此功能。这意味着您这种类型的用户将必须编写以下内容之一:

method(_)

或者这个:

SomeMath(10, _ + 1)

自然,前一种方法更加清晰。

此外,SomeMath(10, new IntMath { def apply(x: Int): Int = x + 1 }) 类型提供了功能类型的单个公共分母,从而提高了互操作性。例如,数学中的函数除了它们的签名外没有任何类型的“类型”,并且只要它们的签名匹配,就可以使用任何函数代替任何其他函数。此属性在编程中也很有用,因为它允许重复使用您已经具有的定义以用于不同的目的,从而减少了转换次数,因此减少了编写这些转换时可能发生的错误。

最后,确实确实存在需要为类函数接口创建单独类型的情况。一个示例是,对于完整类型,您可以定义伴随对象,并且伴随对象参与隐式解析。这意味着,如果应该隐式提供函数类型的实例,则可以利用拥有一个伴随对象并在其中定义一些公共隐式的优势,这将使它们可用于该类型的所有用户而无需其他导入:

FunctionN

这里有一个与类型关联的隐式作用域很有用,因为否则// You might even want to extend the function type // to improve interoperability trait Converter[S, T] extends (S => T) { def apply(source: S): T } object Converter { implicit val intToStringConverter: Converter[Int, String] = new Converter[Int, String] { def apply(source: Int): String = source.toString } } 的用户将需要始终导入某些对象/包的内容以获得默认的隐式定义;通过这种方法,默认情况下将搜索Converter中定义的所有隐式。

但是,这些情况不是很常见。我认为,通常来说,您应该首先尝试使用常规函数类型。如果出于实际原因,您发现确实需要单独的类型,则只有这样,您才应该创建自己的类型。

答案 1 :(得分:1)

另一种选择是不定义任何内容并使函数类型明确:

def apply(x: Int, y: Int => Int): Int = y(x)

通过明确哪些参数是数据对象和哪些参数是函数对象,使代码更具可读性。 (纯粹主义者会说功能语言没有区别,但是我认为这在大多数真实世界的代码中都是有用的区别,特别是如果您来自更具声明性的背景)

如果参数或结果变得更加复杂,则可以将其定义为自己的类型,但函数本身仍是显式的:

def apply(x: MyValue, y: MyValue => MyValue2): MyValue2 = y(x)

否则,B是可取的,因为它允许您将函数作为lambda传递:

SomeMath(1, _ + 3)

A将要求您声明一个新的IntMath实例,该实例更加麻烦且习惯用法较少。