如何编写符合习惯的Clojure(+函数式)代码?

10

我刚开始学习Clojure,虽然我很喜欢这门语言,但是我不知道如何用惯用方式做一些事情。

在使用compojure编写Web应用程序时,下面是我的一个控制器操作:

(defn create [session params]
  (let [user (user/find-by-email (params :email))]
    (if user
        (if (user/authenticate user (params :password))
            (do (sign-in session user)
                (resp/redirect "/home?signed-in=true"))
            (resp/redirect "/?error=incorrect-password"))
        (let [new-user (user/create params)]
          (sign-in session new-user)
          (resp/redirect "/home?new-user=true")))))

我以非常命令的方式写这篇文章。使用了很多的 let / if / do,我不禁想到我正在做一些非常错误的事情。如何使用函数式编程的方式来完成呢?

下面是我尝试做的伪代码:

look if user exists
  if user exists, try to sign user in using password provided
    if password is wrong, redirect to "/?error=incorrect-password"
    if password is correct, sign user in and redirect to "/home?signed-in=true"
  else create user, sign user in, and redirect to "/home?new-user=true"

非常感谢!

3个回答

8
if并没有任何非函数式的问题 - 它是一个完全好用的、纯粹的函数式结构,用于根据条件表达式进行条件判断。然而,如果在同一处出现了太多个if,这可能是一个警示信号,提示你应该使用不同的结构(例如:cond?采用协议的多态性?多方法?高阶函数的组合?) do比较棘手:如果你有一个do,那么就意味着你正在为一个副作用做某些事情,这显然是���函数式的。在你的例子中,sign-inuser/create似乎是造成副作用的罪魁祸首。
对于副作用,你应该怎么办呢?它们有时是必要的,所以挑战在于如何构建代码,以确保副作用得到控制和管理(理想情况下,重构到特殊的状态处理区域中,使其它代码保持干净和纯函数式)。
在你的情况下,你可以考虑:
  • 将"user/authenticate"函数作为输入的一部分传递(例如,在某种上下文映射中)。这将允许您传递测试身份验证函数,例如当您不想使用真实数据库时。
  • 将“已成功验证”标志作为输出的一部分返回。这将被捕获到更高级别的处理程序函数中,该函数负责执行与登录相关的任何副作用。
  • 或者,将“新用户”标志作为输出的一部分返回,并由处理程序函数识别并执行所需的用户设置。

非常感谢您提供如此详细和全面的回答!handler函数/创建函数分解会是什么样子?还有,您所说的“flags”是什么意思?该函数会返回适当的字符串吗? - hurshagrawal
有很多选择。其中一种方式可能是在响应映射中拥有一系列副作用"操作"的列表,处理函数按顺序执行这些操作。您还可以参考一些Ring中间件的工作原理。 - mikera
2
事实上,“if-then-else”结构是由Lisp的创造者John McCarthy发明的,特别是为了使函数式编程更加容易。https://en.wikipedia.org/wiki/McCarthy_Formalism - Paul Legato

5

函数式编程风格鼓励使用高级函数,如map、reduce和filter,并强制您大部分时间处理不可变数据结构。目前为止,您的代码没有任何问题,因为您没有违反任何函数式编程规则。但是,您可以稍微改进一下代码,例如使用if-let结合let和if。


3

这看起来非常合理,而且由于你的副作用只是“必要的”,即添加到会话中,所以我不会认为它非常紧急。但有三个更改我会做,尽管第三个更改是可选的:

  • 像另一个答案建议的那样,将if/let对改为单个if-let
  • 在地图中查找关键字时,请使用(:foo bar),而不是(bar :foo)。这只是标准方法
  • 不要费心为new-user创建一个本地变量,因为你只使用它一次;你可以直接内联它。

还有一个更改我不会做,因为我认为它会降低可读性。然而,这是一个风格和判断的问题,所以我会将其作为让你考虑的事情。注意每个if迷宫的分支都以对resp/redirect的调用结束:你可以将所有这些调用提取到顶层,然后决定传递给它什么参数。结合其他更改,它看起来像:

(defn create [session params]
  (resp/redirect (if-let [user (user/find-by-email (:email params))]
                   (if (user/authenticate user (:password params))
                     (do (sign-in session user)
                         "/home?signed-in=true")
                     "/?error=incorrect-password")
                   (do (sign-in session (user/create params))
                       "/home?new-user=true"))))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接