DEV Community

Cover image for Practical Functional Programming in JavaScript - Side Effects and Purity

Practical Functional Programming in JavaScript - Side Effects and Purity

Richard Tong on July 02, 2020

Edit: This article doesn't do such a great job at communicating what I originally intended, so it has a revision. I recommend you read the revised ...
Collapse
 
miketalbot profile image
Mike Talbot ⭐

That has to be the clearest description of tap() I've seen. Thanks!

Collapse
 
epolanski profile image
Enrico Polanski

Your program is still not pure.

We've isolated the console log and variable mutation side effects to the boundaries of our program by defining them in their own functions logCalledWith, incNumCalls, and logNumCalls

That's not the way you handle side effects in functional programs, you should never have impure functions.

The simplest way to make that program pure is by having incNumCalls return the new state, having the logNumCalls be lazy, meaning functions that return the effect of logging, but don't log.

Logging in functional programs is hard to implement and generally leverages the Writer monad, which, differently from your example, truly keeps the effect outside the boundary.

kseo.github.io/posts/2017-01-21-wr...

Collapse
 
richytong profile image
Richard Tong

That's not the way you handle side effects in functional programs, you should never have impure functions.

Says who? Why am I never allowed to have impure functions?

The simplest way to make that program pure is by having incNumCalls return the new state, having the logNumCalls be lazy, meaning functions that return the effect of logging, but don't log.

This doesn't sound very simple to me. I don't see how returning an effect of logging but not actually logging makes my life any easier. Is it not simpler to just log when you want to log?

Logging in functional programs is hard to implement and generally leverages the Writer monad, which, differently from your example, truly keeps the effect outside the boundary.

Logging in functional JavaScript programs is easy if you don't leverage the Writer monad. Maybe in Haskell the definitive way is to use a Writer monad, but in JavaScript you can just console.log. It's quite straightforward.

Your program is still not pure.

I never said my program is pure. The final add10 is pure, and the other functions are effectful. I said "the final program is a composition of side effecting functions and a pure function". I was never trying to make my entire program pure in the first place. You can't just make your side effects go away. At some point you have to deal with them. Why not deal with them at the boundaries of the same program?

It seems to me you are prescribing the practices you picked up in Haskell onto JavaScript. That is not sensible. Different languages have different boundaries for effects. In Haskell, you straight up declare your effects with the types, so it seems like the rest of your program is pure (which it is, in Haskell). In JavaScript, the effects are implied in the syntax and global functions, for example, console.log very specifically has the effect of writing out to the console.

kseo.github.io/posts/2017-01-21-wr...

I don't see how this belongs here. I don't need Monads to do what I need to do. I especially don't need them to program functionally in JavaScript. In fact, I started my library rubico to double down on functional composition and rebuke the complex hierarchy of effectful types of "traditional" functional programming as prescribed by zealots such as yourself.

Collapse
 
epolanski profile image
Enrico Polanski

Well, you can do what you want, but you shouldn't be calling it functional programming, as it's not really.

Thread Thread
 
thoughtalone profile image
thoughtalone

Lazy Pure Static Functional Programming isn't "functional programming". It's a weird corner of FP introduced in the 80s that has few inherent advantages.

Almost all the ideas Haskell introduces are to solve problems Haskell creates, ie., those due to lazyness.

In a language with a semi-colon you do not need a monad.

Thread Thread
 
mikearnaldi profile image
Michael Arnaldi • Edited

"Functional Programming" as in Programming with Functions can only be pure because a non-pure function is simply NOT a function (it is a procedure). It might be argued that the lazyness is optional, and in fact it is but as soon as you write a side effect inside one of your "functions" and that gets executed you will transform by definition your program to be procedural given the composition of a function with a procedure is a procedure.

Thread Thread
 
richytong profile image
Richard Tong

I would just like to point out this interesting take from the MDN web docs on functions. (link)

A function is a JavaScript procedure—a set of statements that performs a task or calculates a value.

It sounds like they're saying a function and a procedure are the same. Is there a misunderstanding here?

Thread Thread
 
mikearnaldi profile image
Michael Arnaldi

There is a misunderstanding, the definition of function is a binary mapping between sets. The interpretation from MDN is strictly related to the world of programming languages where the term function is improperly used to represent any procedure. It is not the meaning used in "functional programming" where functional refers to a classic function. Sometimes in programming we like to add the term "pure" to denote when a function (from the language perspective) is actually a function.

One thing to note is that all of the nice properties of "pure" functions that are leveraged in FP are almost never respected when functions are not "pure" (or better, not functions)

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

Inspired by this article, I fiddled around to enable a basic functional pipeline in js-coroutines so I could have a runner that started generator functions that yield to do a time check and enable its processing over multiple frames functionality. Wondering though if it would be better off making something for rubico that would do that same as my knowledge is definitely lacking in this area.

Collapse
 
richytong profile image
Richard Tong

I've mulled over supporting generator functions in rubico before, but couldn't find a great use case from my experience. I see your example with a generator function used with async functions here

      const process = pipe(
             parseAsync,
             function * (data) {
                let i = 0
                let output = []
                for(let item of data) {
                    output.push({...item, 
                       total: item.units * item.price,
                       score: complexScore(item)
                    })
                    if((i++ % 100)==0) yield
                }
                return output
             },
             tap(console.log),
             stringifyAsync
         )    

I see how this is useful, but I'm trying to think of all the use cases. The data and output make sense to me - data goes in and output goes out. I see yield in this case freeing up the event loop and splitting the processing up over multiple frames (assuming pipe is fully consuming the generated iterable then returning the output). Are there any other use cases for yield in the context of a generator function in pipe?

I'm a little hesitant to add in generator functions if this is the only use case (which I feel like it's not, I just need help with use cases) because I think I can achieve the same process splitting with async/await.

      const process = pipe(
             parseAsync,
             async function (data) {
                let i = 0
                let output = []
                for(let item of data) {
                    output.push({...item, 
                       total: item.units * item.price,
                       score: complexScore(item)
                    })
                    if((i++ % 100)==0) await Promise.resolve()
                }
                return output
             },
             tap(console.log),
             stringifyAsync
         ) 
Collapse
 
miketalbot profile image
Mike Talbot ⭐

The primary function of yield in js-coroutines is to test the amount of time remaining in the current idle part of a frame. You may also yield promises to await their completion.

js-coroutines is just basically providing a collaborative multi-tasking program runner. It does this by exposing everything it touches as an async function - so actually heavy lifting can be done in any pipe that can await async functions. That's enough

Collapse
 
samrocksc profile image
Sam Clark

Stand up clap....THANK you for the fantastic article.