bind (>>=)
works bind (>>=)
and return
, but I can't imagine where the state comes from. So, probably, I don’t understand how it all works. As a result, I decided to re-examine the monads using the example of Javascript. The plan was the same when I displayed Y Combinator : I took the original task (here it is an interaction with an immutable state clearly), and went all the way to the solution, changing the original code step by step. var stack = []; stack.push(4); stack.push(5); stack.pop(); // 5 stack.pop(); // 4
push
and pop
. What I don’t like is that they change a state. Well, at least I don't like it in this article.5 : 4
. However, in the text of the article I will only bring changes from the previous step. // .concat() .slice() - , , , var push = function (element, stack) { var newStack = [element].concat(stack); return newStack; }; var pop = function (stack) { var value = stack[0]; var newStack = stack.slice(1); return { value: value, stack: newStack }; }; var stack0 = []; var stack1 = push(4, stack0); var stack2 = push(5, stack1); var result0 = pop(stack2); // {value: 5, stack: [4]} var result1 = pop(result0.stack); // {value: 4, stack: []}
pop
and push
return the resulting stack. pop
also returns the value from the top of the stack. Each subsequent stack operation uses a previous version of the stack, but this is not so obvious due to the difference in the representation of the returned values. We can strengthen the duplication of code by normalizing the returned values: make push
return undefined
: var push = function (element, stack) { var value = undefined; var newStack = [element].concat(stack); return { value: value, stack: newStack }; }; var pop = function (stack) { var value = stack[0]; var newStack = stack.slice(1); return { value: value, stack: newStack }; }; var stack0 = []; var result0 = push(4, stack0); var result1 = push(5, result0.stack); var result2 = pop(result1.stack); // {value: 5, stack: [4]} var result3 = pop(result2.stack); // {value: 4, stack: []}
bind
helper function that simply applies the transferred continuation to the result of the stack operation. In other words, it binds a continuation to a stack operation. var bind = function (value, continuation) { return continuation(value); }; var stack0 = []; var finalResult = bind(push(4, stack0), function (result0) { return bind(push(5, result0.stack), function (result1) { return bind(pop(result1.stack), function (result2) { return bind(pop(result2.stack), function (result3) { var value = result2.value + " : " + result3.value; return { value: value, stack: result3.stack }; }); }); }); });
finalResult
is of the same type as the value of an individual push
or pop
operation. We need a uniform interface.push
and pop
push
and pop
, because we want bind
pass it implicitly.push(4, stack0)
we will call push(4)(stack0)
. In Haskell, we would not need this step, because there the functions are already curried. var push = function (element) { return function (stack) { var value = undefined; var newStack = [element].concat(stack); return { value: value, stack: newStack }; }; }; var pop = function () { return function (stack) { var value = stack[0]; var newStack = stack.slice(1); return { value: value, stack: newStack }; }; }; var stack0 = []; var finalResult = bind(push(4)(stack0), function (result0) { return bind(push(5)(result0.stack), function (result1) { return bind(pop()(result1.stack), function (result2) { return bind(pop()(result2.stack), function (result3) { var value = result2.value + " : " + result3.value; return { value: value, stack: result3.stack }; }); }); }); });
bind
for passing stacksbind
deal with passing an argument with an explicit stack. To do this, first, let's make it so that bind
stack as the last parameter, but in the form of a curried function, i.e. let bind
return a function that takes a stack as an argument. Also, now push
and pop
partially applied, that is, we no longer pass them the stack directly, this is now done by bind
. var bind = function (stackOperation, continuation) { return function (stack) { return continuation(stackOperation(stack)); }; }; var stack0 = []; var finalResult = bind(push(4), function (result0) { return bind(push(5), function (result1) { return bind(pop(), function (result2) { return bind(pop(), function (result3) { var value = result2.value + " : " + result3.value; return { value: value, stack: result3.stack }; })(result2.stack); })(result1.stack); })(result0.stack); })(stack0);
bind
so that it analyzes the return value of the stackOperation
function, stackOperation
out the stack, and stackOperation
it to the continuation, which should be a function that accepts the stack. We must also wrap the return value { value: value, stack: result3.stack }
into an anonymous function. var bind = function (stackOperation, continuation) { return function (stack) { var result = stackOperation(stack); var newStack = result.stack; return continuation(result)(newStack); }; }; var computation = bind(push(4), function (result0) { return bind(push(5), function (result1) { return bind(pop(), function (result2) { return bind(pop(), function (result3) { var value = result2.value + " : " + result3.value; // We need this anonymous function because we changed the protocol // of the continuation. Now, each continuation must return a // function which accepts a stack. return function (stack) { return { value: value, stack: stack }; }; }); }); }); }); var stack0 = []; var finalResult = computation(stack0);
result
helper function. In addition, it will hide the view of the state we are storing - a structure with two fields, value
and stack
. var result = function (value) { return function (stack) { return { value: value, stack: stack }; }; }; var computation = bind(push(4), function (result0) { return bind(push(5), function (result1) { return bind(pop(), function (result2) { return bind(pop(), function (result3) { return result(result2.value + " : " + result3.value); }); }); }); }); var stack0 = []; var finalResult = computation(stack0);
return
function in Haskell does. It wraps the result of the calculation in a monad. In our case, it wraps the result in a closure that the stack accepts, and this is precisely the monad of computations with a variable state — a function that takes its state. In other words, result/return
can be described as a factory function that creates a new context with a state around the value that we give it.push
and pop
functions, which actually represent the insides of the monad. So we modify bind
to transfer only the necessary minimum of data to callbacks. var bind = function (stackOperation, continuation) { return function (stack) { var result = stackOperation(stack); return continuation(result.value)(result.stack); }; }; var computation = bind(push(4), function () { return bind(push(5), function () { return bind(pop(), function (result1) { return bind(pop(), function (result2) { return result(result1 + " : " + result2); }); }); }); }); var stack0 = []; var finalResult = computation(stack0);
runState
, evalState
and execState
.State
suffix with a Stack
. // Returns both the result and the final state. var runStack = function (stackOperation, initialStack) { return stackOperation(initialStack); }; // Returns only the computed result. var evalStack = function (stackOperation, initialStack) { return stackOperation(initialStack).value; }; // Returns only the final state. var execStack = function (stackOperation, initialStack) { return stackOperation(initialStack).stack; }; var stack0 = []; console.log(runStack(computation, stack0)); // { value="5 : 4", stack=[]} console.log(evalStack(computation, stack0)); // 5 : 4 console.log(execStack(computation, stack0)); // []
evalStack
is what we need. It starts the monadic calculation, discards the final state, and returns the calculated value. Using this function, we can pull the value out of the monadic context.result
and bind
. The first will work as a factory for the function you created, and the second will provide the outside world with only the necessary data on the monad, and do all the boring work, such as passing a state, using the continuation that will receive the value computed by the monad. Everything that should be inside the monad - remains inside. Just like in OOP - you can even create monadic getters / setters.computation
would look like: computation = do push 4 push 5 a <- pop b <- pop return $ (show a) ++ " : " ++ (show b)
do
notation. It's just sugar for a version that still looks better than in Javascript. Haskell, thanks to support for operator redefinition and laconic lambda expressions, allows for a more readable implementation of monads: computation = push 4 >>= \_ -> push 5 >>= \_ -> pop >>= \a -> pop >>= \b -> return $ (show a) ++ " : " ++ (show b)
>>=
, this is what I called bind
in Javascript, and return
is what I called result
. Yes, return
in Haskell is not a keyword, but a function. In other cases, return
is a unit
. Brian Marick called >>=
decider in his Clojure monad video . Patcher, of course, he called return
.sequence
. Due to the dynamic nature of Javascript, the sequence
can take an arbitrary number of arguments, which are monadic operations that must be executed sequentially, and the last argument an action that must be performed on the result of monadic actions. All non-undefined monadic calculations will be passed to this callback. var sequence = function (/* monadicActions..., continuation */) { var args = [].slice.call(arguments); var monadicActions = args.slice(0, -1); var continuation = args.slice(-1)[0]; return function (stack) { var initialState = { values: [], stack: stack }; var state = monadicActions.reduce(function (state, action) { var result = action(state.stack); var values = state.values.concat(result.value); var stack = result.stack; return { values: values, stack: stack }; }, initialState); var values = state.values.filter(function (value) { return value !== undefined; }); return continuation.apply(this, values)(state.stack); }; }; var computation = sequence( push(4), // <- programmable commas :) push(5), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } ); var initialStack = []; var result = computation(initialStack); // "5 : 4"
sequence
. var computation = sequence( push(4), push(5), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } );
runStack
, evalStack
or execStack
. var initialStack = []; evalStack(computation, initialStack);
push
and pop
produce actions on some global value, while in fact they are always waiting for this value to be passed to them. It's almost like in OOP, when we have this
as the context of the calculation. In our case, this is implemented through currying and partial application, and also indicates a new context in each expression. And, if in OOP the context is called implicit, then using monads you make it even more implicit (if there is one at all). var computation1 = sequence( push(4), push(5), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } ); var computation2 = sequence( push(2), push(3), pop(), pop(), function (pop1, pop2) { return result(pop1 + " : " + pop2); } ); var composed = sequence( computation1, computation2, function (a, b) { return result(a + " : " + b); } ); console.log( evalStack(composed, []) ); // "5 : 4 : 3 : 2"
Source: https://habr.com/ru/post/238171/
All Articles