Does functional programming have a standard construct for this logic?
const passAround = (f) => (x) => {
f(x);
return x;
};
This enables me to compose functions that have side effects and no return values, like console.log
. It's not like a Task because I don't want to represent the state of the side effect.
The SKI combinator calculus might interest you. Let's pretend that
f
is always a pure function:Anyway, the reason why I bring up the SKI combinator calculus is because I want to introduce you to the concept of Applicative Functors. In particular, the
Reader
applicative functor is equivalent to the SKI combinator calculus. TheS
combinator is equivalent to theap
method ofReader
and theK
combinator is equivalent to thepure
method ofReader
.In JavaScript, the equivalent of
Reader
isFunction
. Hence, we can defineap
andpure
for functions in JavaScript as follows:But wait, there's so much more that you can do with applicative functors. Every applicative functor is also a functor. This means that applicative functors must also have a
map
method. ForReader
themap
method is just function composition. It's equivalent to theB
combinator. Usingmap
you can do really interesting things like:The
seq
function is in fact equivalent to the(*>)
method of the Applicative class. This enables a functional style of method cascading.If you are talking about pure functional programming, then you need to challenge this starting point:
In functional programming, there is no such thing. Every function is defined as a transformation on some input into some output.
So the obvious question is, how would you represent
console.log
without a side effect? To answer, we need to challenge another assumption in your question:This is exactly how functional programming represents the problem: consider your input and output to be "the state of the world". In other words, given the state of the world before the function, return the state of the world after the function. In this case, you would be representing the state of the console: given a console with x lines of output, return a console with x+1 lines of output. Crudely, you could write something like this:
The more powerful mechanism generally used for representing this is called a "monad" - a special kind of object which wraps a series of steps along with some extra meaning. In the case of the
IO
monad, each step is wrapped with an action which will transform the state of the world. (I/O is just one of many useful applications of the monad concept.)You write the steps as functions which only know about the "unwrapped" value of some part of that state (e.g. a parameter which ultimately came from user input), and the monad handles the messy details of actually executing that outside the realm of the functional program. So rather than thinking about your input and output as "the state of the world", you think about your input as "a chain of computations", and your output as "a slightly longer chain of computations".
There are many introductions to this that are far better than any I could give, just search for "monad" or "functional programming io".
See also, this answer, this question, and probably many others in the "Related" sidebar auto-generated when you view this question.
So in Haskell terminology, you want this:
Read the type signature as “
passAround
takes a functionf :: a -> m b
, whose result is a monadic action (i.e., something that may have side-effects which can be sequenced in a well-defined order, thus theMonad m
constraint) with arbitrary result-typeb
, and a valuea
to pass this function. It yields a monadic action with result-typea
.”To see what “functional programming construct” this might correspond to, let's first unroll this syntax. In Haskell,
do
sequencing notation is just syntactic sugar for monadic combinators, namely,is sugar for
foo >> bar
. (This is a bit trivial really, the whole thing really only gets interesting when you also bind local results to variables.)So,
>>
itself is shorthand for the general monadic-chaining operator, namelyor
(That backslash denotes a lambda function, in JavaScript it would read
f(x) >>= (y)=>return x
.)Now, what you really want all this for is, chaining multiple actions. In Javascript you would write
g(passAround(f, x))
, in Haskell this is not just a function argument because it's still a monadic action, so you want another monadic chaining operator:g =<< passAround f x
orIf we expand
passAround
here, we getNow, here we can apply the monad laws, namely the associativity law, giving us
and now the left unit law
IOW, the whole composition collapses down to just
f x >> g x
, which could also be written...which is kind of, duh. What of it all? Well, the nice thing is that we can abstract over this monad-rewrapping, with a monad transformer. In Haskell, it's called
ReaderT
. What you would do if you know thatf
andg
both use the variablex
, you could exchangewith
The
ReaderT
value constructor corresponds conceptually to yourpassAround
function.Note that
ReaderT a m c
has the form(ReaderT a m) c
or, ignoring the details,m' c
, wherem'
is again a monad! And, using thedo
syntax for that monad, you can simply writewhich would in JavaScript look, theoretically, like
Unfortunately you can't actually write it this way because unlike Haskell, imperative languages always use the same monad for sequencing their operation (which roughly corresponds to Haskell's
IO
monad). Incidentally, that's one of the standard description of what a monad is: it's an overloaded semicolon operator.What you can certainly do however is implement a monad transformer on dynamic types in the functional part of the JavaScript language. I'm just not sure if it's worth the effort.