如何编写惯用的clojure(+功能)代码?

时间:2013-05-06 00:44:19

标签: clojure functional-programming

我刚开始用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 s / if s / 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 个答案:

答案 0 :(得分:8)

if没有任何不起作用的东西 - 它是一个非常好的,纯函数的构造,可以有条件地评估表达式。但是,在一个地方有太多if可能是一个警示标志,你应该使用不同的结构(例如cond?polymorphism with protocols?multimethods?一个高阶函数的组合?)

do更棘手:如果你做了,那么它意味着你正在为副作用做一些事情,这肯定是无效的。在您的情况下,sign-inuser/create似乎是这里的副作用罪魁祸首。

你应该怎么做副作用?好吧,它们有时是必要的,所以挑战在于如何构建代码以确保副作用受到控制和管理(理想情况下重构为特殊的状态处理危险区域,以便其余代码保持干净和纯粹的功能)。

在您的情况下,您可以考虑:

  • 将“user / authenticate”函数作为输入的一部分传递(例如,在某种形式的上下文映射中)。这将允许您传入测试身份验证功能,例如,当您不想使用真实数据库时。
  • 返回“已成功验证”标志作为输出的一部分。然后,这将由更高级别的处理函数捕获,该函数可能负责执行与登录相关的任何副作用。
  • 或者返回“新用户”标志作为输出的一部分,处理程序功能同样会识别并执行任何所需的用户设置。

答案 1 :(得分:5)

函数式编程风格鼓励使用更高级别的函数,如map,reduce和filter,它们也会强制您在大多数时间处理不可变数据结构。到目前为止,您的代码没有任何问题,因为您没有破坏函数式编程中的任何单一规则。但是,您可以稍微改进一下代码,例如组合let和if if-let。

答案 2 :(得分:3)

这看起来很合理,而且由于你唯一的副作用是“必要的”,即加入会话,我不会称之为非常必要。我会做三个更改,但第三个是可选的:

  • 如建议的另一个答案,请将if / let对更改为单个if-let
  • 在地图中使用(:foo bar)进行关键字查询,而不是(bar :foo)。那就是standard way to do it
  • 不要为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"))))