Erlang中的函数链

问题描述 投票:2回答:3

创建像Active Record或Hibernate这样的ORM会很好,它应该像这样处理链式查询:

User = User:new():for_login(«stackoverflow_admin»):for_password(«1984»):load().

我们应该怎么做?或者就像那样,在一条线上 - 或者至少在精神和意义上相似。

也许有一些预处理工具可以帮助解决这个问题?

erlang
3个回答
5
投票

虽然我发现@zxq9回答如此丰富,但提到在Erlang中模仿类似Java的OOP风格的历史可能会有所帮助。

链接方法需要状态,并且努力将“状态模块”包含到Erlang中:

  • Parametrized Module:一种提议,允许开发人员实现接受参数的模块,并将其作为模块状态保存在其函数中。很久以前它已被添加到Erlang作为实验性功能,但技术委员会decided to remove在R16中对此功能的语法支持,因为它引起的概念和实际不兼容性。
  • Tuple Module:参数化模块的向后兼容性,在Programming Erlang (Second Edition)的第8章中也有详细介绍,但是没有官方文档,它仍然是一个controversial feature,它引入了复杂性和模糊性而没有Erlang方式的强大功能。

在Erlang中模仿具有不同范例(如Ruby和Java)的语言编码风格,这是一种具有不同概念的函数式语言,可能令人兴奋但没有附加价值。


7
投票

“链接”是一种混淆的功能组合形式,有时依赖于返回值,有时直接在状态突变上运行。这不是Erlang的工作方式。

考虑:

f(g(H))

相当于:

G = g(H),
f(G)

但可能相同或不相同:

g(H).f()

在这种形式中,函数被堆叠起来,并且操作“朝向”返回值侧(在大多数编程语言中几乎总是左侧)。然而,在程序员强制OOP作为唯一可用范例(例如Java)的语言中,由于要求所有函数(类方法)基本上都是由命名空间命名的,因此通常不可能没有过多的噪声。类名,无论是否涉及任何对象:

F.f(G.g(h.get_h()))

更典型的是,对数据的Java操作被添加到对象本身,并且尽可能多的数据保存在对象实例中。变换方法没有以相同的方式具有“返回”值,而是改变内部状态,留下相同的对象,但是它的“新版本”。如果我们将变异方法视为“返回对象的新版本”,那么我们可以将点运算符视为一个私有的函数组合运算符,使得值变为“向右流动”,因为变异对象现在具有额外的调用的方法,随着事物的发展,可能会继续改变状态:

query.prepare(some_query).first(100).sort("asc")

在这种情况下,执行“流程”向右移动,但仅仅因为功能组合的概念已经丢失 - 我们沿着一系列变异状态事件向前“滴答”而不是使用实际返回值。这也意味着在这些语言中,如果我们采用堆叠太远,我们可以在执行方向上得到一些非常奇怪的触发器:

presenter.sort(conn.perform(query.prepare(some_query)).first(100), "asc")

快速,一目了然告诉我最重要的内在价值是什么?

这不是一个特别好的主意。

Erlang没有对象,它有进程,并且它们不像你想象的那样工作。 (这里将详细讨论:Erlang Process vs Java Thread。)Erlang进程不能相互调用或相互执行操作 - 它们只能发送消息,生成,监视和链接。而已。因此,“隐式返回”值(例如在变异对象值链的情况下)无法在其上定义操作。 Erlang中没有隐式返回:Erlang中的每个操作都有一个显式返回。

除了显式返回和隔离内存并发进程的计算模型而不是共享内存对象之外,Erlang代码通常被编写为“快速崩溃”。大多数情况下,这意味着几乎任何具有副作用的函数都会返回元组而不是裸值。为什么?因为赋值运算符也是匹配运算符,这也使它成为断言运算符。程序中具有副作用的部分中的每一行通常以断言操作成功或返回预期类型或立即崩溃的方式编写。通过这种方式,我们可以准确地捕获故障发生的位置,而不是继续处理可能有故障或丢失的数据(这在OOP链式代码中始终发生 - 因此严重依赖异常来打破不良情况)。

使用你的代码,我不确定执行(以及我的眼睛,当我读它)是否应该从左向右流动,或者相反:

User = User:new():for_login(«stackoverflow_admin»):for_password(«1984»):load().

在Erlang中可能的等价物是:

User = load(set_password(set_uid(user:new(), "so_admin") "1984"))

但那太傻了。这些神秘的文字来自哪里?我们打算在线调用它们:

User = load(set_password(set_uid(user:new(), ask_uid()) ask_pw()))

如果用户输入无效值(例如什么都没有)或断开连接,或者超时等等,这将是非常尴尬的。当找到一个角落案例时调试也会很难看 - 什么叫什么部分?失败了,与实际问题无关的东西现在正在等待返回值的堆栈上? (这是例外的地方...更多关于下面醒来的噩梦。)

相反,解决这个问题的常用方法类似于:

register_new_user(Conn) ->
    {ok, Name} = ask_username(Conn),
    {ok, Pass} = ask_password(Conn),
    {ok, User} = create_user(Name, Pass),
    User.

我们为什么要这样做?所以我们知道它何时崩溃到底发生了什么 - 这将告诉我们它是如何以及为什么发生的。如果任何返回值不是形状{ok, Value}的元组,那么进程将在那里崩溃(并且大多数时候这意味着与它进行连接 - 这是一件好事)。考虑像Java这样的语言实际需要处理这样的副作用过程的异常程度。长链单线突然变得更多线条:

User =
    try
        User:new():for_login("so_admin"):for_password("1984"):load()
    catch
        {error, {password, Value}} ->
            % Handle it
        {error, {username, Value}} ->
            % Handle it
        {error, db_create} ->
            % Handle it
        {error, dropped_connection} ->
            % Handle it
        {error, timeout} ->
            % Handle it
        %% Any other errors that might possible happen...
    end.

这是不确定(甚至过长)组合的一个超级恼人的结果:它将所有错误情况堆叠在一起,并且必须通过传播异常来处理它们。如果你不在这些调用中的上述错误情况下抛出异常,那么你就不知道出了什么问题,你无法通知进程或其经理执行应该终止,重试等等。一行解决方案至少还有十几行仅添加到这一个过程中,我们甚至没有解决那些错误处理程序中应该发生的事情!

这是Erlang“让它崩溃”理念的光辉。只要我们断言这些假设,代码就可以用只做成功假设的方式编写。错误处理代码可以从其他地方(主管)中提取出来,并且当出现问题时,状态可以恢复到主要业务逻辑之外的已知条件。拥抱它会创建强大的并发系统,忽略它会产生脆弱的晶体。

然而,所有这些的成本是那些单行断言。在并发程序中,这是一个非常有益的权衡。


1
投票

看看BossDB。它是一个编译器链和运行时库,用于通过Erlang参数化模块访问数据库。

© www.soinside.com 2019 - 2024. All rights reserved.