📜 ⬆️ ⬇️

Composition of functions on F # and Scala

Simply speaking what it is all about


I started thinking about writing this article a few weeks ago, after when I tried to explain to my 7 year old child what mathematical functions are. We began by looking at very simple things. It will sound crazy and probably awkward, but I finished my introductory explanation with a narration about the composition of functions. It seemed so logical to explain what functions are, citing examples of their use from the outside world, to talk about composition. The purpose of this article is to show how simple and powerful is the composition of functions. I will begin by reviewing the concept of pure composition and a ground clarification, after which we will try a little curry and have fun with the monads. I hope you will like it.


Function as a small box


Let's imagine the mathematical functions in the form of small boxes (boxes), where each box is able to accept any positive number of arguments, perform any task and return the result. In short, we could introduce the addition function as shown below:



Image 1, alphanumeric representation of the addition function



Image 2, the symbolic representation of the addition function


Let's consider the situation when we need to build and run a bread factory a la all in one. This factory will be built on the principle of requests, where each such request will activate a chain of specific operations and at the final stage will give us the result in the form of ready-made bread. At the beginning we need to define these very specific operations, we will represent each operation as a function / box. Here is a list of higher order operations that we might need:



It is time to organize a bread factory, gathering in a single production chain as shown below:


w -> [Grind] -> [KneadDough] -> [DistributeDough] -> [Bake] -> b 


Image 3, representation of the assembled chain


That's all for now, our chain is ready for work, it is assembled from small pieces, where each piece can be disassembled into separate sub-pieces, and so on. You can model a huge amount of things from the world around us, simply using the concept of composition of functions. It is actually very simple. You can read more theoretical aspects here .


Composition expression


Let's take a look at how to present the production chain described above using javascript:


 var b = bake(distribureDough(kneadDough(grind(w)))); 

Try to imagine how a chain of 10 - 15 functions will look, and this is only one of the possible problems that you may encounter. It is also not quite a composition, since in mathematics, the composition of functions is the one-by-one application of one function to the result of another to obtain the third function. We can achieve this in the following way:


 function myChain1(w) { return bake(distribureDough(kneadDough(grind(w)))); } var b = myChain1(w); 

It looks ridiculous, isn't it? Let's call on the power of functional programming and implement it in a more digestible form. We will operate with more understandable examples. First we need to define what is composition in the functional concept.


Scala version

 implicit class Forward[TIn, TIntermediate](f: TIn => TIntermediate) { def >> [TOut](g: TIntermediate => TOut): TIn => TOut = source => g(f(source)) } 

Version f

Actually, F # already has a default composition operator, you don't need to declare anything. But if you still need to override it, you can do it like this:


 let (>>) fgx = g ( f(x) ) 

The F # compiler is smart enough to assume that you are dealing with functions, so the type of the function described above (>>) will look like:


 f:('a -> 'b) -> g:('b -> 'c) -> x:'a -> 'c 

We put everything together

The solution for the previous task will look like this on Scala :


 object BreadFactory { case class Wheat() case class Flour() case class Dough() case class Bread() def grind: (Wheat => Flour) = w => {println("make the flour"); Flour()} def kneadDough: (Flour => Dough) = f => {println("make the dough"); Dough()} def distributeDough: (Dough => Seq[Dough]) = d => {println("distribute the dough"); Seq[Dough]()} def bake: (Seq[Dough] => Seq[Bread]) = sd => {println("bake the bread"); Seq[Bread]()} def main(args: Array[String]): Unit = { (grind >> kneadDough >> distributeDough >> bake) (Wheat()) } implicit class Forward[TIn, TIntermediate](f: TIn => TIntermediate) { def >> [TOut](g: TIntermediate => TOut): TIn => TOut = source => g(f(source)) } } 

The F # version will be more concise:


 type Wheat = {wheat:string} type Flour = {flour:string} type Dough = {dough:string} type Bread = {bread:string} let grind (w:Wheat) = printfn "make the flour"; {flour = ""} let kneadDough (f:Flour) = printfn "make the dough"; {dough = ""} let distributeDough (d:Dough) = printfn "distribute the dough"; seq { yield d} let bake (sd:seq<Dough>) = printfn "bake the bread"; seq { yield {bread = ""}} (grind >> kneadDough >> distributeDough >> bake) ({wheat = ""}) 

The console output will be:


 make the flour make the dough distribute the dough bake the bread 

Carring


If you are not familiar with the concept of currying, you can find more information here . In this part, we combine two powerful mechanisms from the world of functional programming — currying and composition. Let's consider the situation when you need to work with functions that have more than one parameter and most of these parameters are known before the execution of the function itself. For example, the bake function from the previous part may have parameters such as temperature and duration of baking, which in turn are well known in advance.


Scala:


 def bake: (Int => Int => Seq[Dough] => Seq[Bread]) = temperature => duration => sd => { println(s"bake the bread, duration: $duration, temperature: $temperature") Seq[Bread]() } 

F #:


 let bake temperature duration (sd:seq<Dough>) = printfn "bake the bread, duration: %d, temperature: %d" temperature duration seq { yield {bread = ""}} 

Carring is our friend, let's define one recipe for baking bread.


Scala:


 def bakeRecipe1 = bake(350)(45) def main(args: Array[String]): Unit = { (grind >> kneadDough >> distributeDough >> bakeRecipe1) (Wheat()) } 

F #:


 let bakeRecipe1: seq<Dough> -> seq<Bread> = bake 350 45 (grind >> kneadDough >> distributeDough >> bakeRecipe1) ({wheat = ""}) 

The output in both cases will be as follows:


 make the flour make the dough distribute the dough bake the bread, duration: 45, temperature: 350 

Monadic chains


Can you imagine a situation when something goes wrong in the middle of your chain? Well, for example, the situation when the wire supply of yeast or water becomes clogged and the production of dough is disturbed, or the situation in which the oven breaks and we get a semi-baked mass of dough. Pure composition of functions may be interesting for problems tolerant to failures or to their non-failing counterparts. But what do we do in the above cases? The answer is obvious - use monads, hmm. You can find a bunch of fundamental things about monads on a page on wikipedia . Let's see how monads can be useful in our situation, first we need to define (in F #) or use (in Scala) a special type called Either . The definition in F # may look like a markup union below:


 type Either<'a, 'b> = | Left of 'a | Right of 'b 

Now we are ready to concatenate all elements, for this we need to create an equivalent of the bind monadic operation, which takes the monadic value (M) and the function (f) capable of transforming this value ( f: (x -> M y) ).


F #:


 let chainFunOrFail twoTrackInput switchFunction = match twoTrackInput with | Left s -> switchFunction s | Right f -> Right f let (>>=) = chainFunOrFail 

Scala:


 implicit class MonadicForward[TLeft, TRight](twoTrackInput: Either[TLeft,TRight]) { def >>= [TIntermediate](switchFunction: TLeft => Either[TIntermediate, TRight]) = twoTrackInput match { case Left (s) => switchFunction(s) case Right (f) => Right(f) } } 

The last thing we have to do is make a small adaptation of the above chain in a new, more Either friendly format.


F #:


 let grind (w:Wheat): Either<Flour, string> = printfn "make the flour"; Left {flour = ""} let kneadDough (f:Flour) = printfn "make the dough"; Left {dough = ""} let distributeDough (d:Dough) = printfn "distribute the dough"; Left(seq { yield d}) let bake temperature duration (sd:seq<Dough>) = printfn "bake the bread, duration: %d, temperature: %d" duration temperature Left (seq { yield {bread = ""}}) let bakeRecipe1: seq<Dough> -> Either<seq<Bread>, string> = bake 350 45 ({wheat = ""} |> grind) >>= kneadDough >>= distributeDough >>= bakeRecipe1 

Scala:


 def grind: (Wheat => Either[Flour, String]) = w => { println("make the flour"); Left(Flour()) } def kneadDough: (Flour => Either[Dough, String]) = f => { println("make the dough"); Left(Dough()) } def distributeDough: (Dough => Either[Seq[Dough], String]) = d => { println("distribute the dough"); Left(Seq[Dough]()) } def bake: (Int => Int => Seq[Dough] => Either[Seq[Bread], String]) = temperature => duration => sd => { println(s"bake the bread, duration: $duration, temperature: $temperature") Left(Seq[Bread]()) } def bakeRecipe1 = bake(350)(45) def main(args: Array[String]): Unit = { grind(Wheat()) >>= kneadDough >>= distributeDough >>= bakeRecipe1 } 

The output will look like this:


 make the flour make the dough distribute the dough bake the bread, duration: 45, temperature: 350 

If one of the elements of your chain returns Right with a corresponding error indicator, the subsequent elements of the chain will be simply ignored and the workflow will skip them all and will simply spread the thrown exception from the previous link to the next one. You can independently try to play with scenarios in which there are errors.


Final part


As you can see, there is some magical connection between the theory of categories (the origins of monads) and the composition of functions. The goal of this article is to show how to manage in practice with the presented mechanisms and how to organize your code in a more functional way. You can plunge into more fundamental aspects of the material presented by yourself. I hope this article will be useful for those of you who are looking for how to abandon imperative programming and understand the style of functional thinking, or who simply want to discover the practical aspects of monads and functional composition.


Links



')

Source: https://habr.com/ru/post/283162/


All Articles