Functional programming construct for composing ide

2019-04-08 21:22发布

问题:

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.

回答1:

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.



回答2:

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.



回答3:

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.