clojure 面向对象_面向铁路的编程Clojure和异常处理的原因和方式

clojure 面向对象

AppsFlyer has a broad functionality offering many products. For us as software developers it means writing complicated business flows on a daily basis. Complicated flows can mean basically anything: business logic containing many I/O operations, complex data transformations pipelines, etc. Anything complicated in code already contains potential exceptions, errors and bugs by definition.

AppsFlyer具有提供许多产品的广泛功能。 对于我们作为软件开发人员而言,这意味着每天编写复杂的业务流程。 复杂的流程基本上可以指任何东西:包含许多I / O操作的业务逻辑,复杂的数据转换管道等。按定义,任何复杂的代码已经包含潜在的异常,错误和错误。

What is also important, we at AppsFlyer use Clojure, a functional language, as the primary backend language for production.

同样重要的是,我们在AppsFlyer使用Clojure(一种功能语言)作为生产的主要后端语言。

This raises the question of how to tackle exception handling in a functional manner, which should be both human readable and functionally elegant. This challenge can be solved with a beautiful programming principle — Railway Oriented Programming (ROP), broadly used in other functional languages, but not really common in Clojure. In this blog post I would like to introduce the basics of ROP while hoping to increase the popularity of this powerful yet elegant tool. To discover more advanced features of ROP, please check out Scott Wlaschin’s amazing talk about ROP and F#, which this post is inspired by.

这就提出了一个问题,即如何以功能方式处理异常处理,这种方式应该既易于理解又在功能上应是优雅的。 可以通过优美的编程原理(面向铁路的编程(ROP))解决这一挑战,它广泛用于其他功能语言中,但在Clojure中并不常见。 在此博客文章中,我希望介绍ROP的基础知识,同时希望增加这种功能强大而优雅的工具的受欢迎程度。 要发现ROP的更多高级功能,请查看Scott Wlaschin关于ROP和F#的精彩演讲 ,该帖子的灵感源于此。

幸福的道路 (Happy path)

Let’s say we need to develop a user sign up web page, which will be pretty similar to AppsFlyer’s one. The page contains a form to collect a user’s details. In this article, we’ll focus on the backend part of the flow:

假设我们需要开发一个用户注册网页,该网页与AppsFlyer的网页非常相似。 该页面包含用于收集用户详细信息的表单。 在本文中,我们将重点介绍流程的后端部分:

flow algorithm diagram: happy path

The clojure code reflecting the flow is pretty straightforward:

反映流程的clojure代码非常简单:

(defn create-user
"Gets data from UI, decodes, validates, enriches the data,
saves it to the DB, sends verification email to the user and returns response"
[request]
(-> request
decode-request
validate-user-input
enrich-input
update-DB
send-email
build-response-with-status))

Thanks to clojure’s threading macro the code looks so good. What could possibly go wrong?

感谢clojure的线程宏,代码看起来很棒。 可能出什么问题了?

Image for post

悲伤的道路 (Sad path)

Basically everything:

基本上一切:

flow algorithm diagram: sad path

So instead of our elegant piped code at the beginning we are getting a monster like this:

因此,与其一开始没有优雅的管道代码,不如说是一个怪物:

(defn create-user
"Gets data from UI, decodes, validates, enriches the data,
saves it to the DB, sends verification email to the user and returns response"
[request]
(try
(let [data (decode-data request)
validation-result (validate-user-input data)]
(if (:valid? validation-result)
(let [enriched-data (enrich-input data) ; can't escape this let: bindings used in several places
DB-response (update-DB enriched-data)]
; type of response and sending email depend on DB response status
(if (= (:status DB-response) 201)
; the response depends on sending email
(build-response-with-status enriched-data
(send-email data))
{:success false:error (str "Failed creating user: "
(-> (json/parse-string DB-response true):body:error))}))
validation-result))
(catch Exception e
(log/error "Failed creating user, exception: " e))))

问题 (The problem)

Basically the code works. Sometimes if the code does its job, it’s good enough, and it’s the developer’s right to decide to leave it this way. But it hurts to see this kind of code when writing in functional language: keeping state with lets (as we need the declared bindings in several places), a lot of ifs to check the state of the operations and select the “path” of “success”/”failure”, try-catch with its catch’s performance penalties.

基本上,代码可以工作。 有时,如果代码能够正常工作,就足够了,并且开发人员有权决定以这种方式保留它。 但是用函数式语言编写时看到这种代码很伤人:使用let保持状态(因为我们需要在多个位置声明声明的绑定),使用很多if来检查操作的状态并选择“路径” “成功” /“失败”, try - catch catch的性能损失。

nested `if`s and `else`s

What is more, the code is not readable at all: it’s hard to understand the order of the operations, to detect which “else” line corresponds to which if, and what the overall logic is. The last, but the most important point is that if-else, let as state keeper and try-catch are very imperative by their nature.

而且,这些代码根本不可读:很难理解操作的顺序,检测哪条“ else”行对应于if ,以及总的逻辑是什么。 最后一个,但最重要的一点是, if elselet作为状态守卫者和try - catch ,就其性质而言非常必要。

Sometimes it’s difficult to stay away from imperative approach, but we should remember that we have a choice: the functional paradigm offers us tooling for error handling and flow control.

有时很难远离命令式方法,但是我们应该记住我们有一个选择:功能范式为我们提供了用于错误处理和流控制的工具。

出去的路 (A way out)

What we usually can see in clojure code is splitting the whole flow into 2 things: small “worker” functions which actually do the job and an orchestration function that unites them altogether and is responsible for flow control and error handling.

我们通常在clojure代码中看到的是将整个流程分为两部分:实际完成工作的小型“工作者”功能和将它们完全结合在一起并负责流程控制和错误处理的编排功能。

The orchestration function calls the “workers” in a required order. The workers are piped with the threading macro again, so each of them should return the input the next one expects. Each “worker” throws an exception if something goes wrong inside of it. The orchestration function has a central “catch” point, where all the exceptions are handled:

业务流程功能按要求的顺序调用“工人”。 再次使用线程宏通过管道传递工人,因此每个工人都应返回下一个期望的输入。 如果每个“工人”内部出现问题,都将引发异常。 编排功能具有一个中心“捕获点”,所有异常都在该捕获点进行处理:

(defn create-user
"Gets data from UI, decodes, validates, enriches the data,
saves it to the DB, sends verification email to the user and returns response"
[request]
(try
(-> request
decode-request
validate-user-input
enrich-input
update-DB
send-email
build-response-with-status)
(catch Exception e
(log/error "Failed creating user"
{:error (.getMessage e):request (json/generate-string request)}))))

An example of “worker” function:

“工作者”功能的示例:

(defn- update-DB
"Sends request to DB to create user from the enriched data. Returns input or throws exception"
[input]
(let [{:keys [success message]}
(-> (get-http-response-from-url-with-retries :post
(:DB-api config)
body):body
(json/decode true))]
(if success
input ;returning input to pass it to the next functionthrow (Exception. (str "Failed updating DB: " message))))))

The function is pretty straightforward: it tries sending the user’s details which are passed as an input to the DB. If it succeeds, it returns the input (to pass it to the next function). Otherwise it throws an exception with the details of the event occurred. This trick also helps to bypass the following functions calls, once the error occurs.

该函数非常简单:它尝试将用户的详细信息作为输入传递给数据库。 如果成功,它将返回输入(将其传递给下一个函数)。 否则,它将引发发生事件的详细信息的异常。 一旦发生错误,此技巧还有助于绕过以下函数调用。

So everything looks good, understandable, functional, elegant and readable, right? Right, but… there is always this “but”, and in our case it’s performance. The problem is in using catch a lot: it is expensive, so if the code is throwing a lot of exceptions and using the try-catch for flow control, then performance is an issue.

因此,所有内容看起来都不错,易于理解,功能强大,优雅易读,对吗? 是的,但是……总有这种“但是”,在我们的情况下,就是表现。 问题在于大量使用catch :这很昂贵,因此,如果代码抛出大量异常并使用try - catch进行流控制,那么性能便成为问题。

Yoda
– Do or do not, there is no try. (Yoda)
- 做或者不做,不可以尝试。 (尤达)

Basically, the Clojure community has no consensus about try-catch: some people think it’s essential to use it since clojure itself is written with this method, others consider it non-functional. It is a matter of a personal choice, but for us performance is important.

基本上,Clojure社区对try - catch并没有共识:有些人认为使用它是必不可少的,因为clojure本身是用这种方法编写的,而另一些人则认为它没有功能。 这是个人选择的问题,但对我们而言,性能很重要。

What are we left to do then? Where is the way?

那我们剩下什么呢? 哪里呢?

新希望 (A new hope)

So how can we keep the code containing a lot of operations and conditions clear and functional on one hand, and performance-optimised on the other? Railway oriented programming is the answer! But what do railways have to do with programming? Let’s see!

那么,如何一方面保持包含大量操作和条件的代码清晰,功能正常,另一方面又使性能最佳化呢? 铁路编程是答案! 但是铁路与编程有什么关系? 让我们来看看!

Let’s imagine a function that expects one input parameter, e.g., a validator expecting some data and returning true or false, indicating if the input is valid or not. In other words, the function returns either true or false:

假设有一个函数需要一个输入参数,例如,一个验证器需要一些数据并返回true或false,指示输入是否有效。 换句话说,在函数返回true false:

Image for post

In the functional programming world it is known as Either monad and has analogies in many programming languages.

在函数式编程世界中,它被称为Either monad ,并且在许多编程语言中都有类比。

Depending on the result of validation, we usually decide what to do next: we continue our flow if the input is valid or we log it and quit if the input is invalid (as we have nothing to do with invalid input). So we have one input (track) and two options of output track — continue or quit:

根据验证的结果,我们通常决定下一步该怎么做:如果输入有效,则继续执行流程;如果输入无效,则将其记录下来;如果输入无效,则退出流程(因为我们与无效输入无关)。 因此,我们有一个输入(轨道)和两个输出轨道选项-继续或退出:

two track-function model

So here are the tracks, the railway tracks! Indeed, we can imagine any of our operations as a dual track function:

这就是铁轨,铁轨! 实际上,我们可以将我们的任何操作想象为双轨功能:

different dual track functions

It’s easy to see that every function in this example has one input track (expects one parameter) and has two outcome tracks: success track/happy path and failure track/sad path. The reasonable question here is: what is the sense of all those tracks if we cannot connect them into one flow? The answer is simple: the sense is that we actually can. How? Let’s see together!

可以很容易地看到,此示例中的每个函数都有一个输入轨道(期望一个参数),并且具有两个结果轨道:成功轨道/快乐路径和失败轨道/悲伤路径。 这里的合理问题是:如果我们不能将它们连接成一个流,那么所有这些轨道的意义是什么? 答案很简单:我们确实可以做到。 怎么样? 一起来看吧!

Let’s imagine our happy path with no disruptions, taking just three of our operations to simplify the explanation:

让我们想象一下我们的幸福之路,没有中断,仅需执行以下三个操作即可简化解释:

happy path (the first track)

We get the request, we validate it. The validation passes, and we pass the request data to the DB update function. Let’s say, everything goes smoothly, and we manage to update the DB and get the 200 response. Then we just send the email, which also succeeds, and return the response. The track is straight, passing from one “station” to another with no problem.

我们收到了请求,我们对其进行了验证。 验证通过,然后我们将请求数据传递给数据库更新功能。 假设一切顺利,我们设法更新数据库并获得200响应。 然后,我们仅发送同样成功的电子邮件,并返回响应。 轨道是笔直的,从一个“站”到另一个“站”没有问题。

Once it comes to the sad path, we know that every step can fail, meaning that every “station” has a choice of either success or failure track:

一旦走上了艰难的道路,我们就会知道每个步骤都会失败,这意味着每个“站”都可以选择成功或失败的轨迹:

sad track: every step can fail

Again we start with the request, passing it to the validator. Let’s say, the request is valid. Then we proceed to the following “station” and get to the DB update stage. Let’s say the DB is not available. What are we to do? We do not want to proceed to sending the verification email to the user, as the user was not actually created — there is nothing to verify. The following stages also will not be relevant in this case.

再次,我们从请求开始,将其传递给验证器。 假设请求有效。 然后,我们进入以下“工作站”并进入数据库更新阶段。 假设数据库不可用。 我们接下来干吗? 我们不希望继续向用户发送验证电子邮件,因为实际上不是创建用户的,因此无需验证。 在此情况下,以下阶段也将不相关。

What we actually need to do is to choose a failure track: to log the error and quit the whole flow, bypassing the following steps. This is relevant for every “station” — wherever we fail (in the validator, DB updating, etc.), and every of them has its own failure track. But basically what we would like to do in all of those failure tracks is the same: to log error and quit the whole flow.

我们实际上需要做的是选择一个故障跟踪:绕过以下步骤来记录错误并退出整个流程。 这与每个“站”有关-无论我们在哪里失败(在验证器中,数据库更新等),并且每个人都有其自己的失败轨迹。 但基本上,我们在所有这些故障跟踪中都希望做的是相同的:记录错误并退出整个流程。

how to unite all the failures into one track

So it is reasonable to compose all of the tracks into one unite failure track:

因此将所有轨道组合成一个统一的故障轨道是合理的:

connecting two dual track functions

Thus, we now can connect our “stations” into one duo-track:

因此,我们现在可以将我们的“工作站”连接到一个双轨中:

duo-tack flow

Eventually, all of our functions can be connected together in this manner. Note that we get just one request with input data, which is represented by one success track. After the last “station” of the rail, we can return either two things as two tracks or one response (just wrapping the result with the regular function which returns one thing, e.g. a map for HTTP response).

最终,我们所有的功能都可以通过这种方式连接在一起。 请注意,我们仅收到一个带有输入数据的请求,该请求由一个成功跟踪表示。 在铁轨的最后一个“站”之后,我们可以返回两件事作为两条轨道或一个响应(只需将结果包装在返回一件事的常规函数​​中,例如HTTP响应的映射)。

Okay, so now we understand the rails, stations, connecting them and choosing our paths. Looks good in the pictures, but how is it all related to the code? The implementation is as interesting and easy, as the theory — stay tuned!

好的,现在我们了解了轨道,站点,将它们连接起来并选择了路径。 在图片中看起来不错,但是它们与代码有什么关系? 与理论一样有趣且容易实现-请继续关注!

实作 (Implementation)

First of all, let’s think of the building blocks — our functions which actually do the job and have to decide which track to choose. How can the function decide? Short answer: “with if”. Long answer: the function does not know if the operation will succeed or fail before it actually performs it. Once it’s performed, it is too late to decide though. So the next function should know the result of the previous one to decide, whether to perform the current operation or not: if the previous operation has failed, there is no sense to perform the current operation, so the function needs to choose the failure path. In contrast, if the previous function has succeeded, nothing prevents us from executing the current functionality.

首先,让我们考虑一下构建基块-我们的功能实际上可以完成工作,并且必须决定选择哪条轨道。 功能如何决定? 简短的答案:“ with if ”。 长答案:函数在实际执行操作之前不知道该操作是成功还是失败。 一旦执行完毕,现在就下定决心为时已晚。 因此,下一个功能应该知道上一个操作的结果,以决定是否执行当前操作:如果上一个操作失败,则没有意义执行当前操作,因此该功能需要选择失败路径。 相反,如果先前的功能成功完成,则没有什么可以阻止我们执行当前的功能。

Example: we start with the request, passing it to the validator. There was no previous action, so we are still on a success track, meaning that we should perform the validation. So the function validates the input and has to return some simple indication, if it has succeeded or failed, for the next function (DB updating) to be able to decide whether it should send the data to the DB or not.

示例:我们从请求开始,将其传递给验证器。 之前没有采取任何措施,因此我们仍处于成功轨道上,这意味着我们应该执行验证。 因此,该函数将验证输入,并且必须返回一些简单的指示(无论成功还是失败),以使下一个函数(数据库更新)能够决定是否应将数据发送至数据库。

We can implement this logic in every single function, but we don’t want boilerplate code doing the same for every “worker” function inside our flow. The solution is to create a wrapper function which will do it for every single “worker”:

我们可以在每个函数中实现此逻辑,但是我们不希望样板代码对流程中的每个“工作者”函数都做同样的事情。 解决方案是创建一个包装函数 ,该函数将对每个“工作者”执行此操作:

(defn apply-or-error [f [val err]]
(if err
[val err]
(f val)))

This function gets two parameters: f, which is the next function in our flow, and a vector of val and err, which is the response of our current function. val is the indication of success instructing that the next function should also be executed and passing input value for this function. err is the indication of failure of the current function, instructing that we should take a failure path and preventing the following function’s execution. It is also a description of the error occurred to log and return it in the resulting response if needed. Of course, if the function succeeded, err is nil.

此函数有两个参数: f是流中的下一个函数,以及valerr的向量,这是当前函数的响应。 val是成功的指示,指示还应该执行下一个功能并传递该功能的输入值。 err是当前功能失败的指示,它指示我们应该采取失败路径并阻止后续功能的执行。 它也是对记录错误并在需要时在结果响应中返回错误的描述。 当然,如果函数成功,则errnil

Now it’s obvious what this function does: it is executed before the current “worker” function, and it checks if the previous “worker” has failed or not. In case of success, it calls the following “worker” function and passes the output value of the previous function — the val — as an input parameter. If the error is not nil, it returns a vector of the val and err.

现在很明显,该函数的作用:它在当前“工作者”函数之前执行,并检查先前的“工作者”是否失败。 如果成功,它将调用以下“工作者”函数,并传递上一个函数的输出值val作为输入参数。 如果错误不是nil ,则返回valerr的向量。

You might ask, why return val in case of a sad path. Good question! We do not have to, as in case of failure the next function won’t be executed anyway, so the form of indication of success and failure does not matter anymore. But you still might want to log the val. Why? Imagine, you got a request to add a new user to the DB. Let’s say, the validation has passed, but the DB update has failed. The error that we get from the DB will be something like “Failed updating DB: no connection” or similar. It’s hard to understand from it, which request for which exact user has failed. But once you log the val, which contains the data you send to DB (at least, some user-id for sure), you are able to find the exact reason of failure for the exact user in your logging system.

您可能会问,如果路径val ,为什么要返回val 。 好问题! 我们不必这样做,因为一旦发生故障,下一个功能将永远不会执行,因此指示成功和失败的形式已不再重要。 但是您仍然可能希望记录val 。 为什么? 想象一下,您收到了一个向数据库添加新用户的请求。 假设验证已经通过,但数据库更新失败。 我们从数据库中得到的错误将类似于“更新数据库失败:无连接”之类的错误。 从中很难理解哪个用户的请求失败。 但是,一旦您记录了val ,其中包含发送给数据库的数据(至少,肯定有一些用户ID),便可以在日志记录系统中找到确切用户失败的确切原因。

Also it’s worth mentioning, that vector is used just to simplify the example. It brings additional complexity to the code, making us remember, which place is for success and failure indication (place oriented programming). It’s more convenient to use hash-maps instead: {:success true :error nil}.

同样值得一提的是,向量仅用于简化示例。 它给代码带来了额外的复杂性,使我们记住,哪个位置是成功和失败指示( 面向位置的编程 )。 使用哈希映射代替更方便: { :success true :error nil}

Back to business! So now we have some function, which will decide for us which track to take. How do we use it? We need the decision on every step, so the function should be applied to every “worker” function we have, starting with adding the request and nil as a vector indicating that there was no failure (sure — nothing happened yet):

重新营业! 因此,现在我们有了一些功能,这些功能将为我们确定走哪条轨道。 我们如何使用它? 我们需要在每个步骤上做出决定,因此应将函数应用于我们拥有的每个“工作者”函数,首先将请求和nil作为向量添加,以表明没有失败(确保-尚未发生任何事情):

(clojure.core/->> [request nil]
(apply-or-error decode-request)(apply-or-error validate-user-input)
(apply-or-error enrich-input)
(apply-or-error update-DB)
(apply-or-error send-email)
(apply-or-error build-response-with-status))

It does not make any sense to duplicate for every single function. Here comes power of clojure macros:

为每个功能重复都没有任何意义。 这是clojure 宏的功能

clojureman flying to help with macros power

Don’t be afraid of this terrifying word, it is pretty simple:

不要害怕这个可怕的词,它很简单:

(defmacro =>>
"Threading macro, will execute a given set of functions one by one.
If one of the functions fails, it will skip the rest of the functions and will return a fail cause."
[val & funcs]
(let [steps (for [f funcs] `(apply-or-error ~f))]`(->> [~val nil]
~@steps)))

The macro gets initial val and all the workers funcs. It is pretty much the same to -> that is used inside of it, the only difference is that our macro applies apply-or-error to every single input function. So we don’t need to apply it manually, our macro does the job (as you can see in the previous code snippet which was actually macroexpand of this macro and our “workers”).

宏获得初始val和全体职工funcs 。 它内部使用的->几乎相同,唯一的区别是我们的宏将“ apply-or-error应用于每个输入函数。 因此,我们不需要手动应用它,我们的宏就可以完成工作(正如您在前面的代码片段中看到的那样,它实际上是该宏和我们的“工人”的宏扩展)。

Looks good, huh? Yes, but not 100% yet! We have piped the functions together, we have a “switchman” to decide which path to take, what are we missing? How does apply-or-error decide which path to take? Right, it has the output of the previous function. It means that all the functions should return unified output that will be clear to the switchman. This is the last building block — a wrapper unifying the “workers” output.

看起来不错吧? 是的,但还不是100%! 我们已经将功能通过管道传递在一起,我们有一个“切换员”来决定采用哪条路径,我们还缺少什么? apply-or-error如何决定走哪条路? 正确,它具有先前功能的输出。 这意味着所有功能都应返回统一的输出,这对于开关员来说是清楚的。 这是最后一个构建块–统一“工人”输出的包装器。

Indeed, our regular validator function can return basically anything at any form, from simple boolean to hash-maps: {:valid? bool :error string :invalidity-code int}. It does not look like our standardised railway path indication [val error]. To unify the output, we can wrap the function’s regular output inside the function itself:

确实,我们的常规验证器函数基本上可以以任何形式返回任何内容,从简单的布尔值到哈希映射: { :valid? bool :error string :invalidity-code int} { :valid? bool :error string :invalidity-code int} 。 它看起来不像我们的标准化铁路路线指示[val error] 。 为了统一输出,我们可以将函数的常规输出包装在函数内部:

(defn validate-user-input
"Railway oriented programming (ROP) wrapper. Returns both validation result
and input parameters for the next step, and in case of
success: [result nil]
failure: [result error]"
[input]
(let [validation-result (create-user-input-valid? input)] ;returns `{:valid? bool :error string :invalidity-code int}`
(if (:valid? validation-result)
[input nil]
[input validation-result])))

Looks like we are good to go finally! Let’s put it all together:

看来我们终于可以出发了! 让我们放在一起:

(defn create-account-handler
[data]
(try
(let [[result error] (=>> request
decode-request
validate-user-input
enrich-input
update-DB
send-email
build-response-with-status)]
(if error
(do
(log/error "Failed creating new user"
{:error error :input data})
error)
result)))
(catch Exception e
(log/error "Failed creating new user" {:error e :input data})))

Just look how beautiful our code is! First of all, it’s readable, it’s so easy to see the operations order in the flow. Then, it’s clear what we do with the exceptions returned. It still has a let and an if, but no nested structures — much more beautiful and functional!

看看我们的代码多么美丽! 首先,它易于阅读,很容易看到流程中的操作顺序。 然后,很清楚我们如何处理返回的异常。 它仍然具有letif ,但没有嵌套的结构-更加美观和实用!

i don’t always have errors, but when I have I use ROP

The only question left here is why still use try-catch if we were aiming so heavily to get rid of it?! Well, we indeed managed to eliminate it in everything that depends on us. But there are always things like unexpected errors or third-party libraries exceptions that you cannot handle any other way, because they are designed to be thrown, and you just cannot change it. It is always possible to get them, and we don’t want to crash everything because of it, we want to log them gracefully and go on — this is why try-catchis still here, but hopefully won’t be used.

这里剩下的唯一问题是,如果我们的目标是消除它,为什么还要继续使用try - catch ? 好吧,我们确实设法在所有依赖我们的事物中消除了它。 但是总会有诸如意外错误或第三方库异常之类的事情,您无法以其他任何方式处理,因为它们被设计为可以抛出 ,并且您无法更改它。 总是有可能获得它们,并且我们不想因此而崩溃,我们想要优雅地记录它们并继续进行-这就是为什么try - catch仍然在这里,但希望不会被使用。

现在怎么办? (What now?)

Now you turn to implement ROP! How do you decide whether ROP fits your situation? Easily, there are several signs:

现在您要实施ROP! 您如何确定ROP是否适合您的情况? 容易地,有几个迹象:

  • You have flow with a lot of sequential operations;

    您需要进行大量的顺序操作。
  • You have nested ifs and lets, and you don’t like it;

    您嵌套了iflet ,但您不喜欢它。

  • You have many try-catch, and you care about performance;

    你有很多try - catch ,你关心性能;

  • You used threading macros, but not sure what if something fails;

    您使用了线程宏,但是不确定如果失败了怎么办。
  • You would like to bypass some of the functions is some scenarios;

    您想绕开某些场景中的某些功能;
  • You just would like to functionize your code and try ROP.

    您只想功能化代码并尝试ROP。

How do you start coding it? First of all, you should read before you write. I recommend you to try the “Further reading” section before you add ROP into your production code. The first article is pretty basic, the second and the third are by legendary Scott Wlaschin and explore ROP in detail. If you’d like to optimise your ROP code with monads, consider the links number 4, 5, 6.

您如何开始编码? 首先,您应该在写之前阅读。 我建议您在将ROP添加到生产代码中之前尝试“进一步阅读”部分。 第一篇文章非常基础,第二篇和第三篇文章由传奇人物Scott Wlaschin撰写 ,详细探讨了ROP。 如果您想使用monad优化ROP代码,请考虑链接号4、5、6。

Basically you can take code examples from here or any other article or invent something new. You can use clojure built-in macros like ->>, ->>, some->, some->> and others.

基本上,您可以从此处或任何其他文章中获取代码示例,也可以发明新的东西。 您可以使用clojure内置宏,例如->>->>some->> some->some->>等。

But I highly recommend to take a look into the ready-made ROP libraries (see the list at the end), as the less code you write on your own, the less tests you need to cover it, the less bugs you create. Good luck!

但是我强烈建议您看一下现成的ROP库(请参阅最后的列表),因为您自己编写的代码越少,覆盖它所需的测试就越少,创建的错误也就越少。 祝好运!

结论 (Conclusion)

Railway oriented programming is a principle which can be used in a variety of ways. It is also flexible in implementation as function composition is essential for clojure. What is more, ROP way of validation and flow control can help us minimize the need of excessive error handling. It also offers a unique opportunity to bypass part of the functions in the flow if needed. ROP is power!

面向铁路的编程是可以以多种方式使用的原理。 它的实现也很灵活,因为功能组合对于Clojure是必不可少的。 而且,ROP验证和流控制方式可以帮助我们最大程度地减少过度错误处理的需求。 如果需要,它还提供了独特的机会来绕过流程中的部分功能。 ROP就是力量!

进一步阅读 (Further reading)

  1. “Good Enough” error handling in Clojure

    Clojure中的“足够好”错误处理

  2. Railway Oriented Programming

    铁路导向编程

  3. Railway Oriented programming part II

    面向铁路的编程第二部分

  4. Functors, Applicatives, And Monads In Pictures

    图片中的函子,贴片和单子

  5. A Fistful of Monads

    一把单子

  6. Cats Documentation

    猫文档

Clojure ROP库 (Clojure ROP libraries)

https://github.com/druids/rop

https://github.com/druids/rop

https://github.com/fmnoise/flow

https://github.com/fmnoise/flow

https://github.com/HughPowell/railway-oriented-clj

https://github.com/HughPowell/railway-oriented-clj

Thanks to Nir Rubinstein and Ethan Pransky.

感谢Nir RubinsteinEthan Pransky

翻译自: https://medium.com/appsflyer/railway-oriented-programming-clojure-and-exception-handling-why-and-how-89d75cc94c58

clojure 面向对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值