实施日志流处理分析_通过实施了解流

实施日志流处理分析

Since functional programming has been taking the main stage in programming history (again, LISP predates C), functional concepts have become popularly implemented in JavaScript. Luckily, JS is near perfectly suited to accept these functional ideologies. One such functional concept that’s gained a huge amount of traction in the JS community is the idea of Streams. RXJS calls them Observables, others will refer to them as Signals. Any way you put it, a stream is just a representation of a set of data over time. It’s like a collection, but unlike collections, which are consumed ‘eagerly’ (all at once), streams are ‘lazy’. After a stream is subscribed to, it will give us its data as it arrives, rather than making us request it or processing it all in one chunk. The advantages of this situation is that Streams are reactive and declarative — you define what will happen when data arrives in just one place in your program, and then you don’t have to care about responding to that data elsewhere. Another advantage that streams have over collections is that they can represent infinite sets — an eagerly evaluated collection could never do that, it would crash your program by overflowing the stack.

由于函数式编程已成为编程历史上的主要阶段(同样,LISP早于C),所以函数式概念已广泛地用JavaScript实现。 幸运的是,JS非常适合接受这些功能意识形态。 这样的功能概念在JS社区中获得了广泛的关注,这就是Streams的想法。 RXJS称它们为Observables,其他人将其称为Signals。 不管怎么说,流只是一段时间内一组数据的表示。 这就像一个集合,但是与“急需”(一次全部)消耗的集合不同,流是“惰性”的。 订阅流后,它将在到达时将数据提供给我们,而不是让我们对其进行请求或全部处理。 这种情况的优点是Streams是React性的和声明性的-您可以定义当数据仅到达程序中的一个地方时将发生什么,然后您就不必关心在其他地方响应该数据了。 流优于集合的另一个优点是它们可以表示无限集-急切评估的集合永远无法做到这一点,它会使程序溢出堆栈而使程序崩溃。

To get technical about it, a Stream is also a thing called a Functor (and in fact a Monad, but that’s out of this article’s scope). A Functor is just a fancy box for values. This box exposes a simple interface that has the ability to apply functions to the value that the Functor holds, without worrying about how the functor will process that function. One example of a functor is an array. It has the map function. What the map function designates is that this fancy box has the power to apply any function that operates on an item a to a functor that holds a’s (or F a — functors of a in Haskell-like terms).

为了获得有关它的技术知识,Stream也被称为Functor(实际上是Monad,但这超出了本文的范围)。 函子只是值的花哨框。 此框提供了一个简单的界面,该界面可以将函数应用于Functor所拥有的值,而不必担心functor将如何处理该函数。 函子的一个示例是数组。 它具有map功能。 什么map功能指定的是,这个奇特盒具有该上的项目进行操作的任何功能应用电源a一个存放仿函数a的(或F a -的仿函数a在Haskell类的术语)。

Today I’d like to talk about streams and how to implement our own. By the end of this article we will know how to define a stream. We will implement a way to capture DOM events in a stream, and then operate on those streams to transform our data with ease. My hope is that more people will come to appreciate the beauty of streams and functional programming by reading this article! Without further ado, let’s jump into the juicy innards of streams.

今天,我想谈谈流以及如何实现我们自己的流。 到本文结尾,我们将知道如何定义流。 我们将实现一种方法来捕获流中的DOM事件,然后对这些流进行操作以轻松转换我们的数据。 我希望更多的人通过阅读本文来体会到流和函数式编程的美! 事不宜迟,让我们跳入多汁的溪流内部。

I’ll be calling our streams ‘Signals’. I suppose that’s my attempt to differentiate them from other streams as one that is particularly defined for streaming DOM events. The first thing I’d like to discuss about our Signals is the interface that they will be implementing.

我将我们的流称为“信号”。 我想这是我试图将它们与其他流区别开来的一种,它是为流DOM事件特别定义的。 我要讨论的关于信号的第一件事是它们将要实现的接口。

  1. I’d like to obey the Pointed interface when constructing new Signals. That just means that to instantiate a new Signal, you call the Signal.of method. I’ll actually also have a method called Signal.for to instantiate a Signal for a particular event. The of will primarily be an exhibition of what we can do with a streams.

    构建新的Signals时,我想遵循Pointed接口。 这只是意味着要实例化新的Signal,请调用Signal.of方法。 实际上,我还会有一个称为Signal.for的方法, for为特定事件实例化Signal。 的of将主要是什么,我们可以用流做一个展览。

  2. To be a stream, our Signals must be subscribe-able. That means they have a subscribe instance method that accepts a function parameter. We will be calling that parameter the subscriber.

    要成为流,我们的信号必须是可subscribe 。 这意味着它们具有可以接受函数参数的subscribe实例方法。 我们将把该参数称为订户。

  3. The subscribe method should return an unsubscriber. That’s just a function or an object that upholds a contract to unsubscribe the subscriber function from the stream. If we didn’t do this, we could cause memory leaks by allowing our streams to keep transmitting to dead subscribers.

    subscribe方法应返回一个unsubscriber 。 这仅仅是一个功能或对象,它遵循从流中取消订阅者功能的合同。 如果我们不这样做,则可能通过允许我们的流继续传输到失效的订户而导致内存泄漏。

That pretty much covers what a stream needs to implement to be definitively a stream. That alone wouldn’t be super useful though, it would just make our Signal a fancy event wrapper. It’s the operators that we will define that will make our Signals useful. I will walk us through defining at least map, filter, and reduce in addition to our super awesome event-stream implementation!

这几乎涵盖了流必须最终实现为流所需要实现的内容。 但是,仅凭这一点并没有什么用,它只会使我们的Signal成为事件包装器。 我们将定义的运算符将使我们的信号有用。 除了超棒的事件流实现之外,我还将引导我们至少定义mapfilterreduce

So let’s start simple by creating a factory function for making Signals:

因此,让我们从创建用于生成信号的工厂函数开始简单:

const Signal = (subscribe, x) ⟹ ({
subscribe,
value: x
})

The subscribe parameter becomes the .subscribe instance method. We will expect subscribe to be a function that itself takes our subscriber function as a parameter.

subscribe参数成为.subscribe实例方法。 我们期望订阅是一个本身将订阅者函数作为参数的函数。

Except I don’t like that value is out in the open as an enumerable property on the object returned by Signal. So let’s hide it behind a Symbol:

除非我不喜欢那个value作为Signal返回的对象的可枚举属性公开。 因此,让我们将其隐藏在Symbol后面:

const STATE = Symbol.for('state')const Signal = (subscribe, x) ⟹ ({
subscribe,
[STATE]: { value: x }
})

There, now the end user can’t simply enumerate the keys or use this.value. That’s good, because we only want to modify our state safely through the operators we will define on Signal later.

在那里,最终用户现在不能简单地枚举键或使用this.value 。 很好,因为我们只希望通过稍后将在Signal上定义的运算符安全地修改状态。

Now we need to actually construct a Signal, which I mentioned before will adhere to the Pointed interface.

现在,我们实际上需要构造一个Signal,我之前提到的它将遵循Pointed接口。

Signal.of = function(x) { 
return Signal(function(subscriber){
subscriber(this[STATE].value)
}, x)
}

So we capture the variable x in the of function, then pass it to the subscriber function from the resulting Signal. Notice the parameters that Signal takes — subscribe in particular. What subscribe represents is the logic behind what happens within the Signal, what the Signal does with it’s internal data, and how a function passed in will affect that data. This will be very important later when we define our operators (map, filter, reduce, etc.).

因此,我们捕捉到变量xof功能,然后将它传递从产生的用户函数Signal 。 注意Signal需要的参数-特别是subscribesubscribe代表的是信号内部发生的逻辑,信号对内部数据的处理以及传入的函数如何影响该数据的逻辑。 稍后在定义运算符( mapfilterreduce等)时,这将非常重要。

But what about the unsubscribe portion of the subscription? It would be nice to have another object to encapsulate the unsubscribe functionality. Let’s try that:

但是unsubscribe部分呢? 最好有另一个对象来封装退订功能。 让我们尝试一下:

const noop = () ⟹ {}const Unsubscriber = (cleanup = noop) ⟹ ({
unsubscribe(){ cleanup() }
})Signal.of = function(x) {
return Signal(function(subscriber){
subscriber(this[STATE].value)
return Unsubscriber()
}, x)
}

First let’s talk about noop. It’s a non-operating function — it does nothing. Sounds really useless but it’s actually a staple function in functional programming. You can use it as a placeholder for any function that takes no parameters and returns undefined. Oftentimes a function will take a function, and some of those times you won’t want it to do anything. Our Unsubscriber is exactly one of these cases. It fulfills a contract that requires a function that takes no parameters and returns undefined. We will want our Unsubscriber to perform cleanup operations when necessary (to prevent those memory leaks I mentioned before), but when you sink a simple value into a stream it doesn’t need any cleanup. That’s our opportunity to use noop. It fulfills the contract of taking nothing and returning undefined, and it holds the place of the cleanup function we’ll need to define on other stream operators.

首先让我们谈谈noop 。 这是一个非操作功能-它什么也不做。 听起来确实没什么用,但实际上它是函数编程中的主要功能。 您可以将其用作任何不带参数且返回undefined函数的占位符。 通常,一个函数会使用一个函数,而在某些情况下,您将不希望它执行任何操作。 我们的Unsubscriber正是这些情况之一。 它履行的合同要求一个不带参数且返回undefined的函数。 我们将希望我们的Unsubscriber者在必要时执行清除操作(以防止我前面提到的那些内存泄漏),但是当您将简单值存储到流中时,则不需要任何清除。 这是我们使用noop的机会。 它满足了不采取任何措施并返回undefined的约定,并且占据了我们需要在其他流运算符上定义的cleanup函数的位置。

Otherwise, it’s fairly straightforward what’s happening here — we’ve added the functionality of returning an Unsubscriber. Our Unsubscriber is an object that simply encapsulates our unsubscribe cleanup function. We supply a default parameter, noop, for the Unsubscriber so that we don’t have to supply a cleanup function when there isn’t anything to cleanup.

否则,这是非常简单的事情-我们添加了返回Unsubscriber的功能。 我们的Unsubscriber是一个对象,它简单地封装了我们的unsubscribe清理功能。 我们为Unsubscriber提供了默认参数noop ,这样,在没有任何要清除的内容时,我们不必提供清除功能。

Let’s define our first operator, so that we can start playing around:

让我们定义我们的第一个运算符,以便我们可以开始玩:

const Signal = (subscribe, x) ⟹ {
//...
map(f) {
return Signal(subscriber ⟹ {
return this.subscribe(x ⟹ {
subscriber(f(x))
return Unsubscriber()
})
}, f(this[STATE].value)
},
//...
}

map is a typical operator on functional data types — the concept behind map is ‘take this box of values and apply this function to the data in it’. The Signal’s implementation of map takes care of applying it to the data, so we can just think in simple terms of functions that operate on values. That’s the beauty of FP — simplicity. It seems weird at first, but FP strips away the concrete implementation and focuses on the abstract. Abstraction is a large part of the purpose of programming languages. The more abstract the concept (that is to say, the further away we can be from machine-code) the more understandable that concept is to humans. And that’s what a language is for — human readability! If we can’t understand it we can’t use it, so let’s keep it simple.

map是功能性的数据类型典型的运营商-背后的概念map是“可取的值的此框,在它这个功能应用到数据”。 Signal的map实现会负责将其应用于数据,因此我们可以简单地考虑对值进行操作的函数。 这就是FP的优点-简单。 乍一看似乎很奇怪,但是FP剥夺了具体的实现,而专注于抽象。 抽象是编程语言用途的很大一部分。 这个概念越抽象(也就是说,我们与机器代码的距离越远)对人类来说就越容易理解。 这就是语言的目的-人类可读性! 如果我们无法理解它,就无法使用它,因此让我们保持简单。

Here’s how we use map:

这是我们使用map

const mySignal = Signal.of(3)const sq = x ⟹ x * xmySignal.map(sq).subscribe(console.log) //⟹ Object { unsubscribe() {/*...*/ }, logs 9

See how we don’t need to care that our value is inside of the Signal? We can think simply in terms of it’s inner value when mapping. Also take note of our declarative approach. We define sq and then we can pass it simply by name to map. We could have done this:

看到我们不需要关心我们的价值是否在信号内部吗? 我们可以简单地根据map ping的内在价值来考虑。 还请注意我们的声明式方法。 我们定义sq ,然后我们可以简单地通过名称将其传递给map 。 我们可以这样做:

mySignal.map(x => x * x).subscribe(x => console.log(x))

But it’s much nicer to read it the other way. Passing functions by name is idiomatic functional programming. Another example of keeping things as simple and DRY as possible by defining what you need in one place, one time only.

但是以另一种方式阅读它会更好。 按名称传递函数是惯用函数编程。 通过在一个地方(一次)定义您所需要的东西,使事情尽可能简单和干燥的另一个示例。

Now let’s actually set out to do what we intended — stream DOM events. I mentioned I’d write a pointed function on Signal to construct streams. Let’s do that:

现在让我们开始着手做我们想要的事情-流DOM事件。 我提到我会在Signal上编写一个指向函数以构造流。 让我们这样做:

Signal.for = function(evt, target = document) {
return Signal(subscriber ⟹ { function listener(e){ subscriber(e) } target.addEventListener(evt, listener) return Unsubscriber(() ⟹ {
target.removeEventListener(evt, listener)
})
})
}

So a pretty similar situation to of. We return a Signal and define its subscribe function. Some key differences are the parameters of forevt is the string name of the event we’ll be capturing; target is the DOM element we’ll be listening for, defaulting to any event from within the whole document. The next oddity you may notice is the inner listener function declaration. I put that there because for some reason, removeEventListener doesn’t work for anonymous arrow functions which we *will* want to use. So we wrap up any function passed into another named function that can be removed via removeEventListener. Then it’s a simple matter of adding our subscriber function as a callback to the event. And to prevent memory leaks, we clean up our subscription via the Unsubscriber. See how we optionally can add a cleanup function thanks to noop? Let’s play a bit, then we’ll define some more operators:

因此,一个非常类似的情况of 。 我们返回一个Signal并定义它的订阅函数。 一些关键的区别是for的参数evt是我们将捕获的事件的字符串名称; target是我们将要监听的DOM元素,默认为整个document任何事件。 您可能会注意到的下一个奇怪之处是内部listener函数声明。 我将其放在此处是因为出于某种原因, removeEventListener不适用于我们*将*想要使用的匿名箭头函数。 因此,我们包装了传递给另一个可通过removeEventListener删除的另一个命名函数的任何函数。 然后只需添加我们的subscriber函数作为事件的回调即可。 为了防止内存泄漏,我们通过Unsubscriber清理Unsubscriber 。 看看由于noop我们如何可选地添加清除功能? 让我们玩点儿,然后我们定义更多的运算符:

const div1 = document.body.appendChild(
document.createElement('div')
)
div1.innerHTML = `[x, y] : (0, 0)`const clicks = Signal.for('click')const coords = clicks.subscribe(e ⟹
div1.innerHTML = `[x, y] : (${e.x}, ${e.y})`
)

Each time you click, the Signal will update div1’s innerHTML to reflect the X and Y coordinates of that click event. Cool, right? Let’s extrapolate and define a reduce function. If we do that, we can implement a click counter.

每次单击时,Signal都会更新div1innerHTML以反映该单击事件的X和Y坐标。 酷吧? 让我们外推并定义一个reduce函数。 如果这样做,我们可以实现一个点击计数器。

const Signal = (subscribe, x) ⟹ ({
//...
reduce(rf, seed) {
return Signal(subscriber ⟹ {
this[STATE].value = seed //setup the initial value return this.subscribe(x ⟹ {
//update the state
this[STATE].value = rf(this[STATE].value, x)
subscriber(this[STATE].value)
return Unsubscriber()
})
})
},
//...
})

If you’re familiar with Array.reduce then you know exactly how to use this function. If not, here’s how it works. reduce takes two parameters — rf the reducer function, and seed the initial value. A reducer function follows this format:

如果您熟悉Array.reduce那么您将确切地知道如何使用此函数。 如果没有,这就是它的工作方式。 reduce两个参数rf reducer函数,并为初始值设定seed 。 reducer函数遵循以下格式:

mySignal.reduce((acc, next) ⟹ {/*returns accumulated value*/}

acc is the accumulator. It can be pretty much whatever. Its initial value will be whatever you pass as the seed parameter. Then you define what happens with the accumulator and the next value from the stream. Sometimes this function is called fold, because it combines all the previous values that have come through the stream using the reducer function to turn it into one single latest value. Make sure you return your accumulator, or it won’t work!

acc是累加器。 几乎可以是任何东西。 它的初始值将是您作为seed参数传递的任何值。 然后,您定义累加器会发生什么以及流中的下一个值。 有时将此函数称为fold ,因为它使用reducer函数将流中所有以前的值组合在一起,将其转换为一个最新值。 确保您退还累加器,否则将无法使用!

Now let’s make that click counter I was talking about:

现在,让我正在谈论的点击计数器:

const div2 = document.body.appendChild(
document.createElement('div')
)div2.innerHTML = `count : (0)`const counter =
clicks
.map(_ ⟹ 1)
.reduce((acc, x) ⟹ acc + x, 0)
.subscribe(x ⟹ div2.innerHTML = `count : (${x})`)

Now every time you click, you’ll get the coordinates from your coords stream, and the total accumulated clicks from the counter. Working with and transforming streams is easy when you have good tools like map and reduce to work with. Let’s define another incredibly useful operator for streams — filter.

现在,每次单击时,您都将从coords流中获取coords ,并从counter累计的总点击次数。 当您拥有诸如mapreduce良好工具时,使用和转换流就很容易。 让我们来定义数据流的另一个非常有用的运营商- filter

const Signal = (subscribe, x) ⟹ ({
//...
filter(predicate) {
return Signal(subscriber ⟹ {
return this.subscribe(x ⟹ {
if(predicate(x)) subscriber(x)
return Unsubscriber()
})
})
},
//...
})

The filter operator takes a special function called a predicate. A predicate is pretty much any function that takes a value and returns a boolean value. So by defining a function that takes the type of value that exists in a Signal S a and returns a boolean, we can filter the results of that Signal to reflect the function we defined. This will filter your stream, only allowing values that pass the predicate to be passed on to the subscriber.

filter运算符采用一种称为predicate的特殊功能。 谓词几乎是任何带有值并返回布尔值的函数。 因此,通过定义一个采用信号S a中存在的值类型并返回布尔值的函数,我们可以过滤该信号的结果以反映我们定义的函数。 这将filter您的流,仅允许将通过谓词的值传递给subscriber

Hopefully by now you’ll start to see the pattern of returning a new Signal with the old subscribe baked into it’s behavior. Every operator basically composes the new functionality over the old stream’s behavior. That’s the reason that this approach works at all. We just build up the composition chain with each operator, then when we finally subscribe, the chain unravels and we can get the result of our composition.

希望到现在为止,您将开始看到返回带有旧订阅的新Signal的模式。 每个操作员基本上都是根据旧流的行为来构成新功能的。 这就是这种方法完全有效的原因。 我们只是与每个运算符建立组成链,然后当我们最终订阅时,该链解开,我们可以获得组成的结果。

Now we could filter our click counter. Let’s filter out values over 10:

现在我们可以过滤点击计数器了。 让我们过滤掉10个以上的值:

const count_to_10 = 
clicks
.map(_ ⟹ 1)
.reduce((acc, x) ⟹ acc + x, 0)
.filter(x ⟹ x < 11)
.subscribe(x ⟹ div2.innerHTML = `count : (${x})`)

We could actually write another function takeWhile or takeUntil to describe the functionality we are getting out of filter a bit more accurately, but I’d probably just piggyback filter for that function anyway, so I won’t include that here.

实际上,我们可以编写另一个函数takeWhiletakeUntil以更准确地描述我们要退出filter的功能,但是无论如何,我可能只是背负式filter该功能,因此在此不再赘述。

That pretty much wraps up how we can define our own streams, and hopefully it clarifies how streams work and how we can use them to our advantage.

这几乎涵盖了我们如何定义自己的流,并希望它阐明了流的工作方式以及如何利用它们来发挥自己的优势。

Before we end this article let’s define one more important operator, ap. To ap is to take a fancy box that is holding functions, and apply those functions to another fancy box’s map.

在结束本文之前,让我们定义一个更重要的运算符apap就是拿一个装有功能的精美框,然后将这些功能应用于另一个精美框的map

function err(e) { throw new Error(e) }//...const Signal = (subscribe, x) ⟹ {
//...
ap(ft) {
if(typeof this[STATE].value !== 'function')
err(`Can't ap Signal of non-functions.`)
return ft.map(this[STATE].value)
}
//...
}

Since we defined map already, we basically get ap as a freebie. ap stands for apply. When you have a Signal of functions it can be considered an applicative functor, or one that can apply its contents to the contents of another functor. Pretty nifty! Here’s a pen to demonstrate all of that working:

由于我们已经定义了map ,因此基本上可以将ap作为免费赠品。 ap代表申请。 当您具有信号功能时,可以将其视为适用的函子,也可以将其内容应用于另一个函子的内容。 很漂亮! 这是一支笔来演示所有工作原理:

Hope you enjoyed this episode in my obsession with functional programming, until next time, FP on, folks!

希望您喜欢我对函数式编程的痴迷,直到下一次,FP,伙计们!

翻译自: https://medium.com/swlh/understanding-streams-through-implementation-b999f162605c

实施日志流处理分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值