首页 > 解决方案 > 用于生成变量和函数的 Elm 语法:如何区分?

问题描述

如果这是一个愚蠢的问题,请原谅我,但我一直在阅读“编程榆树”,有一件事情让我觉得有点奇怪:在文本中他展示了一个创建记录的示例,

dog = { name = "Tucker", age = 11 }

然后他展示了一个返回记录的函数

haveBirthday d = { name = d.name, age = d.age + 1 }

对我来说,两者的语法似乎非常相似。编译器如何知道哪个是哪个?在+函数的右侧,这意味着变化,所以它必须是一个函数?事实上,有一个论点,d?还是生成记录和生成函数的区别很明显,只是在这种情况下,它们看起来如此相似?还是以某种我还没有掌握禅的微妙方式,它们实际上是同一回事?(也就是说,“一切都是函数”之类的东西?)

我查看了https://elm-lang.org/docs/syntax#functions - 文档非常用户友好,但简短。是否有任何其他资源可以对语法提供更具指导性的观点(就像这本书对 Haskell 所做的那样)?

感谢您一路上的任何帮助。

标签: elm

解决方案


在以“副作用”为规范的命令式语言中,术语“函数”通常用于描述更恰当地称为过程或子例程的内容。调用时要执行的一组指令,其中执行和重新评估的顺序是必不可少的,因为突变和其他副作用可以随时随地改变任何东西。

然而,在函数式编程中,函数的概念更接近该术语的数学意义,其中它的返回值完全基于其参数计算。对于像 Elm 这样的“纯”函数式语言尤其如此,它通常不允许“副作用”。也就是说,无需通过输入参数或返回值即可与“外部世界”交互的效果。在纯函数式语言中,没有任何参数的函数是没有意义的,因为它总是做同样的事情,一次又一次地计算相同的值只是浪费。没有参数的函数实际上只是一个值。因此,函数定义和值绑定可以仅根据它是否有任何参数来区分。

但也有许多混合编程语言。事实上,大多数函数式语言都是混合的,它们允许副作用但仍然接近函数的数学意义。这些语言通常也没有没有参数的函数,而是使用一种特殊的类型,称为unitor (),它只有一个值,也称为unitor (),用于表示不接受重要输入或不返回任何重要内容的函数。由于unit只有一个值,它不包含重要信息。

许多函数式语言甚至没有接受多个参数的函数。在 Elm 和许多其他语言中,函数只接受一个参数。没有更多也没有更少,永远。您可能已经看到似乎有多个参数的 Elm 代码,但这完全是一种错觉。或语法糖,因为它在语言理论术语中被称为。

当你看到这样的函数定义时:

add a b = a + b

实际转化为:

add = \a -> \b -> a + b

一个接受参数的函数a,然后返回另一个接受参数的函数b,该函数进行实际计算并返回结果。这称为柯里化

为什么要这样做?因为它使得部分应用函数非常方便。你可以省略最后一个或最后几个参数,然后你得到一个函数,而不是一个错误,你可以稍后完全应用它来获得结果。这使您可以做一些非常方便的事情。

让我们看一个例子。要从上面完全苹果化add,我们只需这样做:

add 2 3

编译器实际上将其解析为(add 2) 3,因此我们已经完成了部分应用,但随后立即应用于另一个值。但是,如果我们想添加2一大堆东西并且不想add 2到处写,因为 DRY 之类的怎么办?我们写一个函数:

add2ToThings thing =
  add 2 thing

(好吧,也许有点做作,但请留在我身边)

部分应用程序使我们可以缩短它!

add2ToThings =
  add 2

你明白它是如何工作的吗?add 2返回一个函数,我们只是给它一个名字。在 OOP中已经有很多关于这个奇妙想法的书籍,但他们称之为“依赖注入”,并且在使用 OOP 技术实现时通常会稍微冗长一些。

无论如何,假设我们有一个“事物”列表,我们可以通过像这样映射它来获得一个2添加到所有内容的新列表:

List.map add2ToThings things

但我们可以做得更好!由于add 2实际上比我们给它的名字要短,我们不妨直接使用它:

List.map (add 2) things

好的,但是然后说我们想要filter精确地输出每个值5。实际上,我们也可以部分应用中缀运算符,但我们必须将运算符括在括号中以使其表现得像一个普通函数:

List.filter ((/=) 5) (List.map (add 2) things)

不过,这开始看起来有点令人费解,并且从 we之后filter 向后map读取。幸运的是,我们可以使用 Elm 的管道运算符|>对其进行一些清理:

things
  |> List.map (add 2)
  |> List.filter ((/=) 5) 

由于部分应用,管道操作员被“发现”。没有它,它就不能作为一个普通的运算符来实现,而必须在解析器中作为一个特殊的语法规则来实现。它的实现(基本上)只是:

x |> f = f x

它的左侧有一个任意参数,右侧有一个函数,然后将函数应用于参数。并且由于部分应用,我们可以方便地获取一个函数在右侧传入。

所以在三行普通的 Elm 惯用代码中,我们使用了四次偏应用。没有它和柯里化,我们就不得不写这样的东西:

List.filter (\thing -> thing /= 5) (List.map (\thing -> add 2 thing) things)

或者我们可能想用一些变量绑定来编写它以使其更具可读性:

let
  add2ToThings thing =
    add 2 thing

  thingsWith2Added =
    List.map add2ToThings things

  thingsWith2AddedAndWithout5 =
    List.filter (\thing -> thing /= 5) thingWith2Added
in
thingsWith2AddedAndWithout5

这就是为什么函数式编程很棒的原因。


推荐阅读