As you know I experimented with that a while ago. My code is at http://smalltalkhub.com/#!/~cdlm/Experiments/source
On 31 May 2017 at 15:00, Sven Van Caekenberghe <s...@stfx.eu> wrote: > > > On 31 May 2017, at 14:23, Steffen Märcker <merk...@web.de> wrote: > > > > Hi, > > > > I am the developer of the library 'Transducers' for VisualWorks. It was > formerly known as 'Reducers', but this name was a poor choice. I'd like to > port it to Pharo, if there is any interest on your side. I hope to learn > more about Pharo in this process, since I am mainly a VW guy. And most > likely, I will come up with a bunch of questions. :-) > > > > Meanwhile, I'll cross-post the introduction from VWnc below. I'd be very > happy to hear your optinions, questions and I hope we can start a fruitful > discussion - even if there is not Pharo port yet. > > > > Best, Steffen > > Hi Steffen, > > Looks like very interesting stuff. Would make an nice library/framework > for Pharo. > > Sven > > > Transducers are building blocks that encapsulate how to process elements > > of a data sequence independently of the underlying input and output > source. > > > > > > > > # Overview > > > > ## Encapsulate > > Implementations of enumeration methods, such as #collect:, have the logic > > how to process a single element in common. > > However, that logic is reimplemented each and every time. Transducers > make > > it explicit and facilitate re-use and coherent behavior. > > For example: > > - #collect: requires mapping: (aBlock1 map) > > - #select: requires filtering: (aBlock2 filter) > > > > > > ## Compose > > In practice, algorithms often require multiple processing steps, e.g., > > mapping only a filtered set of elements. > > Transducers are inherently composable, and hereby, allow to make the > > combination of steps explicit. > > Since transducers do not build intermediate collections, their > composition > > is memory-efficient. > > For example: > > - (aBlock1 filter) * (aBlock2 map) "(1.) filter and (2.) map elements" > > > > > > ## Re-Use > > Transducers are decoupled from the input and output sources, and hence, > > they can be reused in different contexts. > > For example: > > - enumeration of collections > > - processing of streams > > - communicating via channels > > > > > > > > # Usage by Example > > > > We build a coin flipping experiment and count the occurrence of heads and > > tails. > > > > First, we associate random numbers with the sides of a coin. > > > > scale := [:x | (x * 2 + 1) floor] map. > > sides := #(heads tails) replace. > > > > Scale is a transducer that maps numbers x between 0 and 1 to 1 and 2. > > Sides is a transducer that replaces the numbers with heads an tails by > > lookup in an array. > > Next, we choose a number of samples. > > > > count := 1000 take. > > > > Count is a transducer that takes 1000 elements from a source. > > We keep track of the occurrences of heads an tails using a bag. > > > > collect := [:bag :c | bag add: c; yourself]. > > > > Collect is binary block (reducing function) that collects events in a > bag. > > We assemble the experiment by transforming the block using the > transducers. > > > > experiment := (scale * sides * count) transform: collect. > > > > From left to right we see the steps involved: scale, sides, count and > > collect. > > Transforming assembles these steps into a binary block (reducing > function) > > we can use to run the experiment. > > > > samples := Random new > > reduce: experiment > > init: Bag new. > > > > Here, we use #reduce:init:, which is mostly similar to #inject:into:. > > To execute a transformation and a reduction together, we can use > > #transduce:reduce:init:. > > > > samples := Random new > > transduce: scale * sides * count > > reduce: collect > > init: Bag new. > > > > We can also express the experiment as data-flow using #<~. > > This enables us to build objects that can be re-used in other > experiments. > > > > coin := sides <~ scale <~ Random new. > > flip := Bag <~ count. > > > > Coin is an eduction, i.e., it binds transducers to a source and > > understands #reduce:init: among others. > > Flip is a transformed reduction, i.e., it binds transducers to a reducing > > function and an initial value. > > By sending #<~, we draw further samples from flipping the coin. > > > > samples := flip <~ coin. > > > > This yields a new Bag with another 1000 samples. > > > > > > > > # Basic Concepts > > > > ## Reducing Functions > > > > A reducing function represents a single step in processing a data > sequence. > > It takes an accumulated result and a value, and returns a new accumulated > > result. > > For example: > > > > collect := [:col :e | col add: e; yourself]. > > sum := #+. > > > > A reducing function can also be ternary, i.e., it takes an accumulated > > result, a key and a value. > > For example: > > > > collect := [:dic :k :v | dict at: k put: v; yourself]. > > > > Reducing functions may be equipped with an optional completing action. > > After finishing processing, it is invoked exactly once, e.g., to free > > resources. > > > > stream := [:str :e | str nextPut: each; yourself] completing: #close. > > absSum := #+ completing: #abs > > > > A reducing function can end processing early by signaling Reduced with a > > result. > > This mechanism also enables the treatment of infinite sources. > > > > nonNil := [:res :e | e ifNil: [Reduced signalWith: res] ifFalse: > [res]]. > > > > The primary approach to process a data sequence is the reducing protocol > > with the messages #reduce:init: and #transduce:reduce:init: if > transducers > > are involved. > > The behavior is similar to #inject:into: but in addition it takes care > of: > > - handling binary and ternary reducing functions, > > - invoking the completing action after finishing, and > > - stopping the reduction if Reduced is signaled. > > The message #transduce:reduce:init: just combines the transformation and > > the reducing step. > > > > However, as reducing functions are step-wise in nature, an application > may > > choose other means to process its data. > > > > > > ## Reducibles > > > > A data source is called reducible if it implements the reducing protocol. > > Default implementations are provided for collections and streams. > > Additionally, blocks without an argument are reducible, too. > > This allows to adapt to custom data sources without additional effort. > > For example: > > > > "XStreams adaptor" > > xstream := filename reading. > > reducible := [[xstream get] on: Incomplete do: [Reduced signal]]. > > > > "natural numbers" > > n := 0. > > reducible := [n := n+1]. > > > > > > ## Transducers > > > > A transducer is an object that transforms a reducing function into > another. > > Transducers encapsulate common steps in processing data sequences, such > as > > map, filter, concatenate, and flatten. > > A transducer transforms a reducing function into another via #transform: > > in order to add those steps. > > They can be composed using #* which yields a new transducer that does > both > > transformations. > > Most transducers require an argument, typically blocks, symbols or > numbers: > > > > square := Map function: #squared. > > take := Take number: 1000. > > > > To facilitate compact notation, the argument types implement > corresponding > > methods: > > > > squareAndTake := #squared map * 1000 take. > > > > Transducers requiring no argument are singletons and can be accessed by > > their class name. > > > > flattenAndDedupe := Flatten * Dedupe. > > > > > > > > # Advanced Concepts > > > > ## Data flows > > > > Processing a sequence of data can often be regarded as a data flow. > > The operator #<~ allows define a flow from a data source through > > processing steps to a drain. > > For example: > > > > squares := Set <~ 1000 take <~ #squared map <~ (1 to: 1000). > > fileOut writeStream <~ #isSeparator filter <~ fileIn readStream. > > > > In both examples #<~ is only used to set up the data flow using reducing > > functions and transducers. > > In contrast to streams, transducers are completely independent from input > > and output sources. > > Hence, we have a clear separation of reading data, writing data and > > processing elements. > > - Sources know how to iterate over data with a reducing function, e.g., > > via #reduce:init:. > > - Drains know how to collect data using a reducing function. > > - Transducers know how to process single elements. > > > > > > ## Reductions > > > > A reduction binds an initial value or a block yielding an initial value > to > > a reducing function. > > The idea is to define a ready-to-use process that can be applied in > > different contexts. > > Reducibles handle reductions via #reduce: and #transduce:reduce: > > For example: > > > > sum := #+ init: 0. > > sum1 := #(1 1 1) reduce: sum. > > sum2 := (1 to: 1000) transduce: #odd filter reduce: sum. > > > > asSet := [:set :e | set add: e; yourself] initializer: [Set new]. > > set1 := #(1 1 1) reduce: asSet. > > set2 := #(1 to: 1000) transduce: #odd filter reduce: asSet. > > > > By combining a transducer with a reduction, a process can be further > > modified. > > > > sumOdds := sum <~ #odd filter > > setOdds := asSet <~ #odd filter > > > > > > ## Eductions > > > > An eduction combines a reducible data sources with a transducer. > > The idea is to define a transformed (virtual) data source that needs not > > to be stored in memory. > > > > odds1 := #odd filter <~ #(1 2 3) readStream. > > odds2 := #odd filter <~ (1 to 1000). > > > > Depending on the underlying source, eductions can be processed once > > (streams, e.g., odds1) or multiple times (collections, e.g., odds2). > > Since no intermediate data is stored, transducers actions are lazy, i.e., > > they are invoked each time the eduction is processed. > > > > > > > > # Origins > > > > Transducers is based on the same-named Clojure library and its ideas. > > Please see: > > http://clojure.org/transducers > > > > >