Functional programming construct for composing identity and side effect
Asked Answered
S

3

6

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.

Surfactant answered 8/9, 2017 at 12:27 Comment(2)
A shorter version would be: f => x => (f(x), x).Excellency
@Excellency An even shorter version would be SK in the SKI combinator calculus.Dani
D
4

The SKI combinator calculus might interest you. Let's pretend that f is always a pure function:

const S = g => f => x => g(x)(f(x)); // S combinator of SKI combinator calculus
const K = x => y => x;               // K combinator of SKI combinator calculus

const passAround = S(K);             // Yes, the passAround function is just SK

console.log(passAround(console.log)(10) + 20);

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. The S combinator is equivalent to the ap method of Reader and the K combinator is equivalent to the pure method of Reader.

In JavaScript, the equivalent of Reader is Function. Hence, we can define ap and pure for functions in JavaScript as follows:

Function.prototype.ap = function (f) {
    return x => this(x)(f(x));
};

Function.pure = x => y => x;

const print = Function.pure.ap(console.log);

console.log(print(10) + 20);

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. For Reader the map method is just function composition. It's equivalent to the B combinator. Using map you can do really interesting things like:

Function.prototype.ap = function (f) {
    return x => this(x)(f(x));
};

Function.pure = x => y => x;

const id = x => x; // I combinator of SKI combinator calculus

Function.prototype.map = function (f) {
    return x => this(f(x));
};

Function.prototype.seq = function (g) {
    return Function.pure(id).map(this).ap(g);
};

const result = console.log.seq(x => x + 20);

console.log(result(10));

The seq function is in fact equivalent to the (*>) method of the Applicative class. This enables a functional style of method cascading.

Dani answered 8/9, 2017 at 16:10 Comment(3)
Thank you for such concrete code examples. They made these concepts much easier to understand.Surfactant
outstanding presentation of work, Aadit – i've yet to dive into application of SKI combinators and I already feel like I know a very practical use case, thanks to this post.Boonie
Wow, thanks so much I really appreciate this. Please write a book for FP in JS, this is the right time for it.Stegman
A
5

If you are talking about pure functional programming, then you need to challenge this starting point:

functions that have side effects and no return values

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:

I don't want to represent the state of the side effect

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:

(x, console) => { return [x, console.withExtraLine(x)]; }

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.

Astrogeology answered 8/9, 2017 at 13:20 Comment(8)
Just for clarification: Not every monad is related to IO. In Haskell IO is a polymorphic type IO a that implements the monad type class (kind of a set of overloaded functions). That is to say you can perform IO without any monadic chaining. For example, putStrLn :: String -> IO () or getChar :: IO Char. To get a sequence of IO actions you need the functorial/monadic operations, though, because Haskell is lazily evaluated.Carvalho
I agree. Monads aren't required for IO. You should add that as a disclaimer to your answer.Dani
@ftor I'm afraid I don't understand what you're saying. Do you mean that you can treat the IO as the "state of the world", and not use the monad abstraction? I'm certainly no expert on any of this, so happy to learn, but I struggle to understand what String -> IO () means - where does the result "come from"?Astrogeology
Let me put it another way (and am a FP learner as well, btw): The effect of the IO monad isn't performing IO, but that you can perform several IO actions in a sequential order. This may sound weird to a dev coming from a strictly evaluated language like Javascript. But in Haskell the lexical order of actions in your code may differ from the order they are actually evaluated. So you don't need a monad to perform IO, but you need a monad to perform several IO operations sequentially.Carvalho
@ftor But what does it mean for putStrLn to "return an IO action" if you don't then chain that up with the rest of your IO actions? Surely you have to at some point give that IO action to the non-functional runtime to actually make the action happen? At which point you're going to give it all your actions combined in some way, except for the trivial case of a program with exactly one I/O action.Astrogeology
Yes, you need a monad for real world IO. Or an applicative, when an IO action doesn't depend on the result of the previous one. Or a functor if you only want to map a pure function over an IO computation. I just wanted to point out that IO and monad aren't the same thing.Carvalho
@ftor OK, I know that not all monads are I/O, and have edited to hopefully make that clearer; the part that confused me was when you talked about not all I/O being monadic. I guess that's a lesson for another day. :)Astrogeology
Thank you for such a thoughtful answer. I'm new to this and had forgotten about the IO monad. I'm very clumsy with these concepts at the moment, so returning the monad and then getting the original x out of the monad is disappointingly complex when i just want to call a callback and pass the value through. But, like I said, I take that as a sign of my lack of mastery.Surfactant
D
4

The SKI combinator calculus might interest you. Let's pretend that f is always a pure function:

const S = g => f => x => g(x)(f(x)); // S combinator of SKI combinator calculus
const K = x => y => x;               // K combinator of SKI combinator calculus

const passAround = S(K);             // Yes, the passAround function is just SK

console.log(passAround(console.log)(10) + 20);

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. The S combinator is equivalent to the ap method of Reader and the K combinator is equivalent to the pure method of Reader.

In JavaScript, the equivalent of Reader is Function. Hence, we can define ap and pure for functions in JavaScript as follows:

Function.prototype.ap = function (f) {
    return x => this(x)(f(x));
};

Function.pure = x => y => x;

const print = Function.pure.ap(console.log);

console.log(print(10) + 20);

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. For Reader the map method is just function composition. It's equivalent to the B combinator. Using map you can do really interesting things like:

Function.prototype.ap = function (f) {
    return x => this(x)(f(x));
};

Function.pure = x => y => x;

const id = x => x; // I combinator of SKI combinator calculus

Function.prototype.map = function (f) {
    return x => this(f(x));
};

Function.prototype.seq = function (g) {
    return Function.pure(id).map(this).ap(g);
};

const result = console.log.seq(x => x + 20);

console.log(result(10));

The seq function is in fact equivalent to the (*>) method of the Applicative class. This enables a functional style of method cascading.

Dani answered 8/9, 2017 at 16:10 Comment(3)
Thank you for such concrete code examples. They made these concepts much easier to understand.Surfactant
outstanding presentation of work, Aadit – i've yet to dive into application of SKI combinators and I already feel like I know a very practical use case, thanks to this post.Boonie
Wow, thanks so much I really appreciate this. Please write a book for FP in JS, this is the right time for it.Stegman
T
1

So in Haskell terminology, you want this:

passAround :: Monad m => (a -> m b) -> a -> m a
passAround f x = do
   f x
   return x

Read the type signature as “passAround takes a function f :: 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 the Monad m constraint) with arbitrary result-type b, and a value a to pass this function. It yields a monadic action with result-type a.”

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,

     do
      foo
      bar

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,

passAround f x = f x >> return x

>> itself is shorthand for the general monadic-chaining operator, namely

passAround f x = f x >>= const (return x)

or

passAround f x = f x >>= \y -> return x

(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 or

passAround f x >>= g

If we expand passAround here, we get

(f x >>= \y -> return x) >>= g

Now, here we can apply the monad laws, namely the associativity law, giving us

f x >>= (\y -> return x >>= g)

and now the left unit law

f x >>= (\y -> g x)

IOW, the whole composition collapses down to just f x >> g x, which could also be written

 do
  f x
  g x

...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 that f and g both use the variable x, you could exchange

f :: a -> m b
g :: a -> m c

with

f' :: ReaderT a m b
f' = ReaderT f
g' :: ReaderT a m c
g' = ReaderT g

The ReaderT value constructor corresponds conceptually to your passAround function.

Note that ReaderT a m c has the form (ReaderT a m) c or, ignoring the details, m' c, where m' is again a monad! And, using the do syntax for that monad, you can simply write

 runReaderT (do
     f'
     g'
  ) x

which would in JavaScript look, theoretically, like

 runReaderT (() => {
      f';
      g';
    }, x)

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.

Terrazas answered 8/9, 2017 at 15:27 Comment(3)
...imperative languages always use the same monad for sequencing their operation (which roughly corresponds to Haskell's IO monad). That is an interesting way to think about it.Carvalho
Of course it's not quite accurate because these languages also have “implicit sequencing”, by allowing side-effectful functions to be invoked in the arguments of other functions.Terrazas
Thank you so much for such a detailed response. It might be worth implementing this transformer as a way to develop understanding. :)Surfactant

© 2022 - 2024 — McMap. All rights reserved.