Skip to content

Instantly share code, notes, and snippets.

@newswim
Last active November 22, 2021 23:24
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save newswim/4668aef8a1f1bc0dabe8 to your computer and use it in GitHub Desktop.
Save newswim/4668aef8a1f1bc0dabe8 to your computer and use it in GitHub Desktop.
Douglas Crockford's Monads and Gonads
Monads / (mo problems)
Taken from Category Theory and imported into Haskell.
Has the strange quirk of being un-explainable.
The monad solves a particular problem that Haskell has
but it turns out to be a generally useful concept.
It starts with Functional Programming. There are two senses:
- programming with functions
- pure functional programming (mathematical)
Found its way into Fortran II (1958)
FUNCTION name(parameters)
DIMENSION ...
COMMON ...
name = expression
RETURN
END
It had types (integer and real).
If you wanted to make something common, you would
call it COMMON.
The RETURN statement didn't have an expression on it
so to tell FORTRAN what value to return, you would
have an assignment to the name of the function.
Instead of curly braces, there was just END to mark the end.
- - - - - - Pure Functional Programming - - - - - -
Functions in programming languages behave nothing like
mathematical functions, because a math function always
returns the same value. They are idempotent, immortal.
You can think of pure functions as maps.
The purpose is that mathematical functions are easier to
reason about.
"Programming without side-effects"
It's easy to turn JavaScript into a pure functional language:
> Remove assignment, loops (use recursion instead),
> freeze all array literals and object literals.
> Remove `Date` and `Math.random` (not mathematical)
> Remove the DOM
This however doesn't really work. How does one do I/O
in a language where nothing changes?
* Memoization
* Caching -- possibly the hardest thing in programming
In the real world, everything changes all the time, so
Immutability makes it hard to interact with the world.
...along came Haskell, and the application of...
- - - - - - Monads - - - - - -
Monads are a loophole in the function contract.
The Function Contract says that every time you call
a function with the same arguments, you get the same result,
and cause no side effects.
But, if you pass a function as a parameter to a function,
every function is different, in the sense of; new invocations,
closing over something its never closed over before, etc.
You're constantly in a state of newness.
This creates the "illusion of immutability"
In Haskell, they came up with the I/O monad.
The Socratic Haskell Programmer
-- "Suppose you have a function, and you want to trace it...."
-- "In order to learn monads, you must first learn Haskell"
-- "...then, you have to learn Category Theory"
In truth, Haskell is a great language -- there is much that
it can teach you. But, you don't need Haskell to learn Monads.
(more buts); ... But! you must first learn JavaScript.
> function unit(value)
> function bind(monad, function(value))
/* All three functions return a monad */
Ok, so whats a monad? It's an object.
`unit` takes a function, and returns an object...
Wait, that sounds like a constructor?
Yes.
- - - - - - Monad Axioms - - - - -
```
bind (unit(value), f) ==== f(value)
bind(monad, unit) ==== monad
bind(bind(monad, f), g)
====
bind(monad, function(value) {
return bind(f(value), g);
})
```
These first two axioms describe the relationship
between bind and unit. The third one (powerful one)
describes composition.
Composition turns out to be tHe ThiNg.
So, monads are usually described in the function form,
but it's more convenient to describe them as methods.
```
bind(monad, func) // the OO transform
monad.bind(func) //
```
In JavaScript, we can be doing things "method space" or
in "function space" and, generally, we choose the once
that is most convenient.
- - - - - - - The Macroid - - - - - -
* n: a macro-like thing used for making other functions
```
function MONAD() {
return function unit(value) {
var monad = Object.create(null);
monad.bind = function (func) {
return func(value);
};
return monad;
};
}
```
Context is much more important that 'what' a thing is.
Walking through.. this "macroid" will take a function, and
return an object with a bind method.
var unit = MONAD();
var monad = unit("Hello world.");
monad.bind(alert);
This is commonly called "The Identity Monad"
Here are the Axioms, rewritten in method forms.
```
unit(value).bind(f) ==== f(value)
monad.bind(unit) ==== monad
monad.bind(f).bind(g)
====
monad.bind(function (value) { // "the Ajax monad"
return f(value).bind(g);
})
```
- - - - - Some examples - - - - -
"The Ajax Monad"
> > Interstate (2001)
> new Interform('text')
> .moveTo (100, 100)
> .setSize (400, 32)
> .moveInside()
> .setBgColor('pink')
> .select()
> .setZIndex(20000)
> .on('escapekey', 'erase')
* * * * *
> > ADsafe (2007)
> var input = dom.q("input_text")
> .on('enterkey', function (e) {
> dom.q('#ROMAN_RESULT')
> .value(roman(input
> .getValue()));
> input
> .select();
> })
> .focus();
this allows for the querying of DOM, but
it doesn't return a node. Rather, a monad
which wrapped that node.
Monads have a potentially nice security quality.
In practice, the method notation is simpler. Contrast:
monad.bind(func)
monad.bind(func, [
a, b, c // array of arguments
])
vs.
monad.method()
monad.method(a, b, c)
- - - - - Extending the Macroid - - - - -
function MONAD() {
var prototype = Object.create(null);
function unit(value) {
var monad = Object.create(prototype);
monad.bind = function (func, args) {
// return func(value, ...args)); /* ES6 */
return func.apply(undefined,
[value].concat(Array.prototype
.slice.apply(args || [])));
};
if (typeof modifier === 'function') { // the Maybe monad
modifier(monad, value);
}
return monad;
}
unit.method = function (name, func) {
prototype[name] = func;
return unit;
};
/* better: */
unit.lift = function (name, func) {
prototype[name] = function (...args) {
return unit(this.bind(func, args));
};
return unit;
};
return unit;
}
It fulfills the monad axiom because it:
- wraps the calling of bind by:
- taking function and calling bind on it
- then takes the result of bind, and calls `unit` on it
This gives the benefit of hiding all of the modadic (heh) operations.
,applying this to the Ajax monad -->
var ajax = MONAD()
.lift('alert', alert);
var monad = ajax("Hello world.");
monad.bind(alert);
monad.alert();
we can now call alert via monad because it's been bound
thanks to the .lift method.
monad ==== ajax
- - - - - null (the Maybe Monad) - - - - -
Null Pointer Exception
usual cause: you didn't do a null pointer check.
It's similar to NaN, but for null values.
We've added to the Macroid to check for functions
and apply them automatically. Example use case:
var maybe = MONAD(function (monad, value) {
if (value === null || value === undefined) {
monad.is_null = true;
monad.bind = function () {
return monad;
}
}
});
var monad = maybe(null);
monad.bind(alert);
Now we get a clear indication of a function's value;
if val === null, returns null monad, rather that *nothing*
This powerful monad lets you write apps w/o null checks.
- - - - - - Concurrency - - - - -
"Threads are evil"
Computers are getting better at it, but languages
generally are not. And neither are programmers.
Threads are very easy to get started with, but
very difficult to reason about later.
Design error warning!
Threads are subject to race conditions, deadlocks, etc.
This is the fault of Java(tm).
At the system level, they are a neccesary evil.
At the application level, they're just evil.
Reliability being reliant on a concurrency mechanism
is unsafe, and that's exactly what threads are.
-- enter Turn Based Programming --
It's what we have in the browser and in places like
most UI frameworks, Nodejs, Twisted, Elko (servers).
* Single-threaded. Race-free. Deadlock free.
"Never wait, never block, finish fast"
No other turn can end, until that one ends.
Can be event-driven (browser) or message-driven (Node).
Asynchronicity can still be hard to manage..
particularly if you have a bunch of events that
are serially dependent on each other. The naive
approach is to have nested event handlers. But
that can get very convoluted.
-- enter Promises --
* Promises are an excellent mechanism for
managing asynchronicity.
* A promise is an object that represents a
possible future value.
* Every promise has a corresponding resolver
that is used to ultimately assign a value
to the promise.
* A promise can have one of three states:
'kept', 'broken', 'pending'.
Promises came out of Futures, which came out of
the Actor model.
It's created 'pending' and can move to one of
the other two states, after that it will
never change again.
* A promise is an event generator. It fires its
event when the value of the promise is ultimately
known.
* At any time after the making of a promise,
event handling functions can be registered
with the promise, which will be called
in order with the promise's value when
it is known.
* A promise can accept functions that will be
called with the value once the promise has
been kept or broken.
* (promise.when(success, failure)) returns
another promise for the result of your
'success' function.
* * * * New Promise Macroid * * * *
// Make a vow
var my_vow = VOW.make();
.keep(value)
.break(reason)
.promise
.when(kept, broken) // returns another promise
* * * * Filesystem API * * * *
We've been doing filesystems wrong since Fortran.
A great way is for the FS to return a promise, then
go do other things when it's waiting for the promise
to come back.
read_file(name)
.when(function success(string) {
...
}, failure);
This also provides a failure function.
Q: Why not throw an exception?
A: The nature of turns.
-- Exceptions --
* Exceptions modify the flow of control by
unwinding the state.
* In turn based system, the stack is emptied
at the end of every turn.
* Exceptions cannon be delivered across
turns.
* Broken promises can.
It's like time travel, you cannon throw something into
a previous call stack, that turn has already ended.
We can't go back, we can only go forward... and the
mechanism for doing that is promises. The failure
branch of the promise is the way you send exceptions
into the future. Additionally, they can cascade....
-- Breakage flows to the end --
my_promise
.when(success_a)
.when(success_b)
.when(success_c, failure);
success_a gets the value of my_promise
success_b gets the value of success_a
success_c gets the value of success_b
unless any promise breaks:
failure gets the reason
@dmitriz
Copy link

dmitriz commented Aug 5, 2019

Except the unfortunate confusion that Promise is a Monad (it isn't):
https://twitter.com/Dmitri145/status/1131026857065771008

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment