【问题标题】:Step-by-step connection between a Scala high-order function to provided examplesScala 高阶函数与提供的示例之间的逐步连接
【发布时间】:2012-02-26 15:36:41
【问题描述】:

我很难弄清楚如何从 Scala 高阶函数定义跳转到提供的示例。它是在this slide showslide 81 上提供的。

高阶函数定义如下:

trait X[A] { def map[B](f: A => B): X[B] }

以下是提供的示例:

(1 to 10) map { x => x * 2 } // evaluates to Vector(2, 4, ..., 20)
(1 to 10) map { _ * 2 }      // shorthand!

嗯?!我在这里缺少一些步骤。我知道这些示例可能同时利用了函数定义和一些 Scala 细节。我只是没有足够的阅读 Scala 和做出连接假设的经验。

我的背景是 Java OO。我现在正在学习 Scala 和函数式编程。这不是我不理解的第一个这样的例子。这是我第一次觉得自己有勇气发帖,知道自己看起来很无知。

我确实尝试过研究这个。首先,我查阅了 Scala 的“圣经”"Programming in Scala 2nd Edition",并试图从那里理解 if(第 165-9 页)。然后,我在 StackOverflow 上进行了搜索。我发现了几个在该地区讨论的链接。但是,没有任何东西真正向我展示了 Scala 高阶函数定义和提供的示例之间的联系,这种联系以映射到本幻灯片中特定实例的方式。

这是我在 StackOverflow 上找到的:

  1. Scala: Workshop Advice
  2. More on generic Scala functions
  3. Scala: How to define "generic" function parameters?

我现在才意识到我跳过了 Google,直接来到了 StackOverflow。嗯。如果你用谷歌搜索并找到正确的链接,我很乐意看到它。我已经没有时间筛选所有使用猴子单子、胚胚等术语的 Google 链接,同时让我更加困惑并且不太可能尝试弄清楚这一点。

【问题讨论】:

  • 难怪......这些例子完全与假特征定义相矛盾。请注意,X[A].map 的结果应该是 X[B],但在示例中,Range.map 的计算结果为 Vector[Int]!发生这种情况是因为Rangemap 首先没有该类型签名。我建议你看看@retronym 的答案,它完全忽略了这个例子,并从基本的类定义中重新解释了这个概念。
  • 圣心皮隧道综合症!这么多答案。太棒了!感谢 Daniel 的指导。

标签: java scala higher-order-functions


【解决方案1】:

我认为以下只是为了显示 Scala 的一些集合属性而提供的示例签名。特别是它没有显示任何实现,因此您无法真正连接所有点。 而且它实际上与示例并不一致......所以,这可能是令人困惑的。

trait X[A] { def map[B](f: A => B): X[B] }

我会这样理解:给定一个集合类 X 超过 A 类型的元素:

  • 它有一个map 函数,该函数在B 类型上参数化
  • map 函数采用函数 f 将单个 A 转换为单个 B
  • mapB 类型的元素上返回相同类型 X 的集合。

然后跳转到例子说明使用:

(1 to 10) map { x => x * 2 }

所以,连接点:

  • 集合X是(1到10)的类型,这里是Range
  • f: A => Bx => x * 2,它被推断为一个接受 Int 并返回 Int 的函数。
  • 给定签名,您会认为会返回 Range 而不是 Int,但实际上返回的是 IndexedSeq

一个更好的例子可能是:

List(1, 2, 3).map(i => i + "!") // a List[Int]
// returns a List[String]: List("1!", "2!", "3!") 

【讨论】:

  • 感谢您的解释。而且我喜欢您指出该特征与提供的示例不一致。阅读您的答案很容易,而且完全有意义。
【解决方案2】:

高阶函数(或方法)是一个函数/方法,它要么将函数作为其参数,要么将函数作为其结果,或两者兼而有之。

在这种情况下,它是一个名为map 的方法,定义在列表、数组以及许多其他类型的容器上。当在 1 to 10 上调用时,1 to 10 是从 1 到 10 的数字范围,在 Scala 中由 Range[Int] 表示,它会一一遍历它们并将函数(已作为参数传入的函数)应用于每个数字范围内。此函数的结果会累积在一个新容器中 - 在本例中为 Vector[Int],它作为 map 方法的结果返回。

所以(1 to 10) map { x => x * 2 }(1 to 10).map(x => x * 2) 的语法糖,将x => x * 2 应用于1 to 10 中的数字。您可以将其视为回调函数。你也可以这样写:

(1 to 10).map( new Function1[Int, Int] {
   override def apply(x: Int) = x * 2
})

【讨论】:

  • 我真的很喜欢你的最后一个例子。这有助于我更多地了解简化背后发生的事情(减少代码样板)。
【解决方案3】:

让我们用 map 方法定义一个数据类型,一个单链表。

sealed abstract class MyList[+A] {
  def map[B](f: A => B): MyList[B]  // higher order function declaration.
  def head: A
  def tail: MyList[A]
}
case class Cons[A](head: A, tail: MyList[A]) extends MyList[A] {
  def map[B](f: A => B): MyList[B] = Cons[B](f(head), tail.map(f))
}
case object Nil extends MyList[Nothing] {
  def map[B](f: Nothing => B): MyList[B] = this
  def head = sys.error("head on empty list")
  def tail = sys.error("tail on empty list")
}

列表要么是空的,要么是单个值(head)与列表的其余部分(tail)配对。我们将这两种情况表示为类层次结构,从密封的父类MyList 扩展。

注意Cons#map的实现,我们使用了参数f两次,第一次是执行head的函数,第二次是传递给tail.map的递归调用。

语法f(head)f.apply(head) 的简写,值f 是定义apply 方法的类Function1 的一个实例。

到目前为止,一切都很好。让我们构造一个列表。

val list: MyList[Int] = Cons(1, Cons(2, Nil))

现在,我们想通过转换每个元素将Ints 的列表转换为Strings 的新列表。在 Java 中,您将显式匿名子类化 Function1,如下所示。

// longhand:
val stringList1: MyList[String] = list.map[String](new Function1[Int, String] {
  def apply(a: Int): String = a.toString
})

这在 Scala 中是合法的,但信噪比不是很好。让我们改用匿名函数语法。

val stringList2: MyList[String] = list.map[String]((a: Int) => a.toString)

我们可以更进一步,省略显式类型注释,编译器有足够的信息来推断它们。

首先,我们根据list的元素类型来推断参数a的类型。

val stringList3: MyList[String] = list.map[String](a => a.toString)

像这样真正简单的函数也可以用占位符语法来表达。无需声明参数,只需编写代码并将_ 用于任何未知数量。这样做,还允许推断 stringList4 的类型:

val stringList4 = list.map(_.toString)

【讨论】:

  • 您使用abstract class 而不是trait 的原因是什么?尤其是性能有区别吗?
  • 在调用 trait 中定义的方法时,理论上的性能损失很小——它所混合的类将参数转发给 trait。这很少成为问题,因为 HotSpot 可以内联它。不管性能如何,我认为抽象类是这里最自然的选择。
  • @retroynm Tyvm 为您解答。我真的很感谢你创造了一个更好的问题来连接这些点。我认为我的大部分困惑来自于该特征在技术上与示例没有特别相关的事实。
【解决方案4】:

让我们专注于定义在特征 X 上的 map 方法

def map[B](f: A => B): X[B]

好的,这是一个带有一个参数的方法,ff(冒号后的位)的类型是A => B。这是一个函数类型;它是 trait Function1[A, B] 的简写,但我更愿意根本不考虑这些特征,而只是将其视为可以为给定 A 值生成 B 的东西

所以:函数A => B 是接受A 类型的单个实例并产生B 类型的单个实例的东西。以下是一些声明它们的示例

val f = (i: Int) => i.toString //Int => String
val g = (_ : String).length    //String => Int, using placeholder syntax

所以现在想想X[A] 是什么;它可以是一个集合类型,例如List。如果您拥有List[String] 和上面的String => Int 函数g,那么您显然可以通过将函数应用于列表中的每个元素来获得List[Int] ,用结果构造一个新列表。

所以现在,你可以说:

strings map g //strings is a List[String]

但 Scala 允许您匿名声明函数。这意味着什么?好吧,这意味着您可以在使用时内联声明它,而不必将其声明为 val 或 var。通常这些被称为“lambdas”。您感到困惑的语法是此类函数的两个选项。

strings map { (x: String) => x.length }
strings map { x => x.length }
strings map { _.length }
strings map ( _.length )

这些基本上都是一样的。第一个明确声明传递给 map 的函数。因为 scala 有类型推断,你可以在这个用例中省略函数输入的类型。使用占位符语法 _ 代替标识符 x 并且在您只需要引用输入一次的情况下是一个很好的糖。在许多情况下,您可以使用括号代替大括号,但多语句函数除外。

【讨论】:

  • 您的最后一个示例(以“字符串映射”开头的 4 个代码行)显示了简化的进展(删除代码样板)非常有帮助。更多的点连接。
  • 这很有趣;几年前,我对完全相同感到迷惑,但现在它已成为第二天性。斯卡拉的一件事;很容易得出结论,在您感到困惑的地方,有一些特殊的语言正在发生,具体到确切的问题。几乎从来没有这样!
【解决方案5】:

您的示例参考了 Scala 集合框架,它本身对类型系统进行了一些复杂的使用,以在转换集合时产生最具体的类型。现在,the mechanism 允许这样做很难掌握,并且与示例并不真正相关。高阶函数就是a function or method,它以other functions 作为参数(或返回)。由于添加了类型参数并且没有提及 Scala 集合框架对隐式的使用,该示例有些模糊。

【讨论】:

  • Tyvm。这有助于我建立联系;即特征来自 Scala 集合库。下面的行利用了库中的许多此类函数定义。
【解决方案6】:

不确定你没有得到什么,但解释一下例子:

trait X[A] { def map[B](f: A => B): X[B] }

trait 就像一个 Java 接口,也可以有具体的方法。

X 是特征名称,[A] 是类型参数——想想 Java 泛型 <A>。 (通常AB 等用于集合中的元素类型,T 用于其他地方,但这只是一种约定。)

该 trait 指定了一个名为 map 的成员,这是一个带有另一个类型参数 [B] 的方法,并采用类型为 A => B 的函数参数,返回类型为 X[B]。这里是抽象的,因为没有方法体。

您可能缺少的一点是A => BFunction1[A, B] 的缩写。 Function1 是带 1 个参数的函数对象的类型。 (A, B) => CFunction2[A, B, C] 等的缩写。您可以在 Java 中创建自己的 Function 类型 - 这是一个有趣的练习。函数对象本质上只是一个具有apply 方法的对象,从一些参数产生结果。

(1 to 10) map { x => x * 2 }    

这些包括无点表示法,其中a.method(b) 写为a method b。所以toRichInt 上的一种方法,采用Int 并产生RangemapRange 上的一个方法,采用 Function1 参数(请记住,函数只是 Function1 类型的对象)。

=> 也用于编写函数本身(除了在类型级别,如上所述)。所以下面都是一样的,都是Int => Int类型的对象:

(x: Int) => x + 1
new Function1[Int, Int] { def apply(x: Int) = x + 1 }
  // note Function1 is a trait, not a class, 
  // so this the same as `new Object with Function[Int, Int]`
new (Int => Int) { def apply(x: Int) = x + 1 }

Scala 使用类型推断,因此如果上下文需要特定的函数类型(或任何其他参数化类型),例如

,您无需自己添加所有类型
val f : Int => Int  =  x => x + 1
val f : Int => Int  =  _ + 1

希望您能明白这个下划线符号的含义。下划线很有用,否则总会有一些重复,因为函数定义的 RHS 必须使用 LHS 的参数。另一个例子可能是将String 映射到其长度的函数:

val f: String => Int = _.length

由于通常会推断出 val 的类型,因此您可以只提供必要的类型注释

val f = (_: String).length

这可能有点令人困惑,因为语法糖和类型推断意味着有多种方法可以编写相同的东西,但是一旦你得到它,你会发现它可以让你的生活更轻松并减少噪音。如果您还没有,请在 REPL 中好好利用这些。

【讨论】:

  • 哇!那是一个非常详细的解释。我现在知道该特征与这两个示例错误地相关(根据 Daniel Sobral 和其他几张海报)。我现在开始理解所有的“简化”(样板),这恰好是一个不平凡的案例。
【解决方案7】:

     斯卡拉:(1 to 10) map { x => x * 2 }
英语:取值,从 1 到 10,然后将每个值乘以 2。

注意事项:

  • (1 到 10),Scala 认为这是整数的集合,特别是 Range[Int]。它可以转换为另一种集合类型,例如。 (1 to 10).toList

  • map,是小写的。想想动词,从事物映射到另一个事物。

  • {x => x * 2},被花括号包围。这意味着它是一个没有名字的函数,一个匿名函数

  • 下划线 (_) 可以代替 x => x


     斯卡拉:trait X[A] { def map[B](f: A => B): X[B] }
英语:
我们定义了一个可以添加到 X 类中的特征,类型 A。
它有一个方法,该方法接受一个值并将其映射到新类 X 的另一个值。

注意:

  • X[A]X[B] 是相同的集合类型,但可以有不同类型的元素,例如。 `(1 到 10).toList map { _.toSTring } 将 List[Int] 映射到 List[String]。

  • f: A => B,这意味着map接受一个函数作为参数,它有一个类型为A的参数并返回类型B。

  • map 定义在所有 Scala 集合类型中。您通常不会自己定义。

【讨论】:

  • Tyvm 为您解答。我终于很清楚地理解了这个特征。而且我现在了解到,该特征并未映射到幻灯片中该特征下方提供的示例。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-01
  • 1970-01-01
  • 2022-09-28
  • 1970-01-01
相关资源
最近更新 更多